Playlists


Project Description:

zkp-co-learning is a Zero-Knowledge Proofs (ZKP) collaborative study and creation project that has been running for three terms. Since February 2023, we have studied https://zkiap.com/, zk-learning.org, plonkathon codes and more with over 300 participants. We are currently preparing advanced content for zk-learning.org.(like Nova、STARK...)

zkp-co-learning offers bounties to incentivize learners for their technical sharing, actively becoming community maintainers (Maintainer), and for organizing knowledge in the ZKP field. Together, we aim to improve ZKP Public Goods.

Problem Statement:

Currently, there are challenges related to high barriers to entry, difficulty in learning in the ZKP field, lack of learning communities, and the overwhelming and opaque nature of information. We intend to address these issues specifically through zkp-co-learning.

Team Background:

  • Qi Zhou: Founder of ETHStorage
  • 郭宇@Secbit: Founder of Secbit, https://github.com/sec-bit, https://secbit.io/
  • Kurt Pan: Ph.D. in Cryptography from Fudan University, https://github.com/kurtpan666 / https://cryptography.land/
  • Harry L: co-Founder of Rebase Community.
  • Shirlene 孝羽: director of Creators Co-learning Community
  • Demian: zkp-co-learning community maintainer, former JD.com algorithm engineer, https://github.com/Demian101
  • Our Maintainers: dream@Scroll, Po@EthStorage, 0xhhh@EthStorage, Frank Jz Liu, miles, 白菜, KEEP, CJ, 笃行, 阳小雪, 啊咪咪小熊, 饭卡里还有不少钱呢...
  • sponsoring agency: [Antalpha Labs]

Vision and Mission:

  1. Continue to recruit Maintainers to collaboratively manage the community and answer questions.
  2. Operate technical media and Twitter to expand influence.
  3. Utilize zk technology to support a broader range of web3 applications.

Roadmap:

We plan to accomplish the following tasks within 6 months:

  1. Launch the zkp-co-learning.xyz website and aim for: 1.1 Comprehensive ZKP learning path sorting and a clear, seamless learning guide. 1.2. zk Hackthon information gathering, allowing everyone to showcase and discuss ideas, and team up for Hackthon. 1.3. Distribute bounties, allowing anyone interested to participate and contribute knowledge and content.
  2. Complete research on at least 5 open-source projects for EF PSE. (https://www.appliedzkp.org/projects)
  3. Host a Hackthon or Hackerhouse to put theory into project practice.
  4. Develop at least one zk application project with specific needs and use cases.
  5. Organize and open-source the vast amount of zk materials that have accumulated and scattered within the current co-learning GROUP.
  6. Attract at least 200 more people to participate in zkp technology learning and engage at least 20 developers in the development of zkp projects.

Project Description:

zkp-co-learning 是一个已经持续了 3 期的 ZKP 共学共创项目,从 2023 年 2 月至今,我们与 300+ 学员共同研究了 https://zkiap.com/ ,zk-learning.org ,plonkathon 代码等,目前正在筹备 zk-learning.org 的进阶内容

zkp-co-learning 针对学员的技术分享,积极成为社群维护者(Maintainer),ZKP 领域的知识整理,都会提供对应的 bounty 来激励他们,共同完善 ZKP Public Goods

Problem Statement

目前,针对 ZKP 领域学习门槛高、难度大,学习社群缺乏,信息庞杂不透明的情况,我们意图通过 zkp-co-learning 来针对性地解决这些问题

Team Background

as above.

愿景

  1. 继续招募 Maintainer 来共同维护社区,解答大家的问题
  2. 运营技术媒体与 Twitter,扩大影响力
  3. 利用 zkp 技术支持更广泛场景的 web3 应用

Roadmap

我们计划使用 5 个月的时间完成以下事务:

  1. 上线 zkp-co-learning.xyz 网站,完成:1. ZKP 完整学习路径梳理,清晰无痛的学习指南 2. zkp Hackthon 信息收集,让大家展示交流 ideas,组队 Hackthon 3. bounty 放送,让每个感兴趣的人都能参与其中,进行学识和内容的贡献
  2. 完成至少 5 个 EF PSE 的开源项目 Research
  3. 举办一次 Hackthon 或 Hackerhouse 来将理论付诸项目实践
  4. 完成至少 1 个具有需求和使用场景的 zkp 应用项目的研发
  5. 整理与开源目前共学 GROUP 里面沉淀散落的大量 zkp 资料
  6. 再吸引至少 200 人参与到 zkp 技术的学习中,让至少 20 名开发者参与到 zkp 项目的研发中

What are we doing now?

进阶研究小组:

如果二维码过期请找 @Demian 索要 ~

投稿

欢迎大家把自己在写的内容放在这里, [zk-everything] 我们会同时排版到Antalpha Labs的公众号下.(最好留下联系方式,方便后续跟进)

[Github Discussion] 使用方法

(tbl)

Themes !

  • 自由选题,完成学习并分享可得 Bounty ,推荐 PSE 开源项目 ~
    • [Meeting PSE Share]
    • [PSE opensource projects list]
  • 目前法国 🇫🇷 巴黎 ZKML Hackerhouse 火热进行中 🔥
  • 年底土耳其 ZKP HackerHouse 等你来 !!

理解 PLONK(一):Plonkish Arithmetization

算术化是指把计算转换成数学对象,然后进行零知识证明。 Plonkish 算术化是 Plonk 证明系统特有的算术化方法,在 Plonkish 出现之前,主流的电路表达形式为 R1CS,被 Pinocchio,Groth16,Bulletproofs 等广泛采用。2019 年 Plonk 方案提出了一种看似复古的电路编码方式,但由于 Plonk 方案将多项式的编码应用到了极致,它不再局限于算术电路中的「加法门」和「乘法门」,而是可以支持更灵活的「自定义门」与「查表门」。

我们先回顾一下 R1CS 的电路编码,也是相关介绍最多的算术化方案。然后我们对比引入 Plonkish 编码。

算术电路与 R1CS 算术化

一个算术电路包含若干个乘法门与加法门。每一个门都有「两个输入」引脚和一个「输出」引脚,任何一个输出引脚可以被接驳到多个门的输入引脚上。

先看一个非常简单的算术电路:

img20230414162317

这个电路表示了这样的一个计算:

电路中有4个变量,其中三个变量为输入变量 ,一个输出变量 ,其中还有一个输入为常数,其值为

一个电路有两种状态:「空白态」和「运算态」。当输入变量没有具体值的时候,电路处于「空白态」,这时我们只能描述电路引线之间的关系,即电路的结构拓扑。

接下来的问题是,我们要先编码电路的「空白态」,即编码各个门的位置,和他们之间引线连接关系。

R1CS 是通过图中的乘法门为中心,用三个「选择子」矩阵来「选择」乘法门的「左输入」、「右输入」、「输出」都分别连接了那些变量。

我们先看看图中最上面的乘法门的左输入,可以用下面的表格来描述:

这个表格只有一行,因此我们可以用一个向量 来代替,表示乘法门的左输入连接了两个变量, 。记住,所有的加法门都会被展开成多个变量的相加(或线性组合)。

再看看其右输入,连接了一个变量 和一个常数值,等价于连接了 的两倍,那么右输入的选择子矩阵可以记为

这里同样可以用一个行向量 来表示,其中的 即为上图中电路的常数引线。

最后乘法门的输出按照上面的方法可以描述为 ,即输出变量为

有了三个向量 ,我们可以通过一个「内积」等式来约束电路的运算:

这个等式化简之后正好可以得到:

如果我们把这几个变量换成赋值向量 ,那么电路的运算可以通过「内积」等式来验证:

而一个错误的赋值向量,比如 ,则不满足「内积等式」:

左边运算结果为 ,右边运算结果为 。当然,我们可以验证 也是一组合法(满足电路约束)的赋值。

并不是任何一个电路都存在赋值向量。凡是存在合法的赋值向量的电路,被称为可被满足的电路。判断一个电路是否可被满足,是一个 NP-Complete 问题,也是一个 NP 困难问题。

这里例子中的两个乘法门并不相同,上面的乘法门是左右输入中都含有变量,而下面的乘法门只有一边的输入为变量,另一边为常数。对于后者这类「常数乘法门」,后续我们也把他们看作为特殊的「加法门」,如下图所示,左边电路右下的乘法门等价于右边电路的右下加法门。

那么如果一个电路含有两个以上的乘法门,我们就不能用 三个向量之间的内积关系来表示运算,而需要构造「三个矩阵」的运算关系。

多个乘法门

比如下图所示电路,有两个乘法门,他们的左右输入都涉及到变量。

c

这个电路表示了这样的一个计算:

我们以乘法门为基准,对电路进行编码。第一步将电路中的乘法门依次编号(无所谓编码顺序,只要前后保持一致)。图中的两个乘法门编码为 #1#2

然后我们需要为每一个乘法门的中间值引线也给出变量名:比如四个输入变量被记为 ,其中 为第二个乘法门的输出,同时作为第一个乘法门的右输入。而 为第一个乘法门的输出。于是我们可以得到一个关于变量名的向量:

该电路的「空白态」可以用下面的三个矩阵来编码:

其中 为乘法门的数量,而 大致为引线的数量。每一个矩阵的第 行「选择」了第 个乘法门的输入输出变量。比如我们定义电路的左输入矩阵

其中第一个乘法门的左输入为 , 第二个乘法门的左输入为 。右输入矩阵 定义为:

其中1号门的右输入为 ,第二个乘法门的右输入为 。最后定义输出矩阵

我们把所有的引线赋值看作为一个向量: (这里用字母 ,取自 Assignments 首字母)

在上面的例子中,「赋值向量」为

于是我们可以轻易地检验下面的等式

其中符号 为 Hadamard Product,表示「按位乘法」。展开上面的按位乘法等式,我们可以得到这个电路的运算过程:

请注意,通常「赋值向量」中需要一个固定赋值为 的变量,这是为了处理加法门中的常量输入。

优缺点

由于 R1CS 编码以乘法门为中心,于是电路中的加法门并不会增加 矩阵的行数,因而对 Prover 的性能影响不大。R1CS 电路的编码清晰简单,利于在其上构造各种 SNARK 方案。

在 2019 年 Plonk 论文中的编码方案同时需要编码加法门与乘法门,看起来因此会增加约束的数量,降低 Proving 性能。但 Plonk 团队随后陆续引入了除乘法与加法外的运算门,比如实现范围检查的门,实现异或运算的门等等。不仅如此,Plonk 支持任何其输入输出满足多项式关系的门,即 Custom Gate,还有适用于实现 RAM 的状态转换门等,随着查表门的提出,Plonk 方案逐步成为许多应用的首选方案,其编码方式也有了一个专门的名词:Plonkish。

Plonkish 算术门

回看下例子电路,我们把三个门全都编号, ,同时把加法门的输出值也标记为变量

显然,上面的电路满足三个约束:

我们定义一个矩阵 来表示约束( 为算术门的数量):

为了区分加法和乘法,我们再定一个向量 来表示运算符

于是我们可以通过下面的等式来表示三个约束:

如果把上面的等式代入并展开,我们可以得到下面的约束等式:

化简后得:

这正好是三个算术门的计算约束。

总结下,Plonkish 需要一个矩阵 来描述电路空白态,而所有的赋值则写入了 矩阵。对于 Prover 和 Verifier 的交换协议, 是 Prover 的 witness,属于秘密知识,对 Verifier 保密, 矩阵代表了一个实现双方约定共识的电路描述。

不过仅仅有 矩阵是不足以精确描述上面的例子电路。

复制约束

比较下面两个电路,它们的 矩阵完全相同,但它们却完全不同。

两个电路的区别在于 是否被接入了 #1 号门。如果让 Prover 直接把电路赋值填入 表格,一个「诚实的」Prover 会在 两个位置填上相同的值;而一个「恶意的」Prover 完全可以填上不同的值。如果恶意 Prover 在 也填入不同的值,那么实际上 Prover 证明的是上图右边的电路,而非是和 Verifier 共识过的电路(左边)。

我们需要增加新的约束,强制要求右边电路图中 。这等价于我们要求 Prover 把同一个变量填入表格多个位置时,必须填入相等的值

这就需要一类新的约束——「拷贝约束」,即 Copy Constraint。Plonk 采用「置换证明」保证 表格中多个位置上的值满足拷贝关系。我们继续用上面这个电路图的案例来说明其基本思路:

设想我们把 表格中的所有位置索引排成一个向量:

然后把应该相等的两个位置互换,比如上图中要求 。于是我们得到了下面的位置向量:

然后我们要求 Prover 证明: 表格按照上面的置换之后,仍然等于自身。置换前后的相等性可以保证 Prover 无法作弊。

再来一个例子,当约束一个向量中有三个(或多个)位置上的值必须相同时,只需要把这三个(或多个)位置的值进行循环移位(左移位或者右移位),然后证明移位后的向量与原向量相等即可。比如:

如果要证明 ,那么只需要证明:

在经过置换的向量 中, 依次右移交换,即 放到了原来 的位置,而 放到了 的位置, 则放到了 的位置。

如果 ,那么 所有对应位置上的值都应该相等,可得: ,即 。这个方法可以适用于任意数量的等价关系。(后续证明两个向量相等的方法请见下章)

那么如何描述电路赋值表格中的交换呢?我们只需要记录 向量即可,当然 向量也可以写成表格的形式:

加上 ,空白电路可以描述为 ,电路的赋值为

再比较

R1CS 的 表格的宽度与引线的数量有关,行数跟乘法门数量有关。这个构造相当于把算术电路看成是仅有乘法门构成,但每个门有多个输入引脚(最多为所有引线的数量)。而 Plonkish 则是同等对待加法门与乘法门,并且因为输入引脚只有两个, 所以 表格的宽度固定,仅有三列(如果要支持高级的计算门,表格可以扩展到更多列)。这一特性是 Plonk 可以利用 Permutation Argument 实现拷贝约束的前提。

..., and thus our linear contraints are just wiring constraints that can be reduced to a permutation check.

按照 Plonk 论文的统计,一般情况下,算术电路中加法门的数量是乘法门的两倍。如果这样看来, 表格的长度会三倍于 R1CS 的矩阵。但这个让步会带来更多的算术化灵活度。

电路验证协议框架

有了电路空白结构的描述和赋值,我们可以大致描述下 Plonk 的协议框架。

首先 Prover 和 Verifier 会对一个共同的电路进行共识, 。 假设电路的公开输出为 ,而 为秘密输入。

Prover 填写 矩阵(Verifier 不可见):

其中增加的第四行是为了增加一个额外的算术约束: ,把 值显示地表示在 矩阵中。

相应的那么 Prover 和 Verifier 共识的 矩阵为

其中第四行约束,保证 ,可以把 代入下面的算术约束,可得 ,即

为了保证第一行的 也必须为 ,这就需要在 矩阵中添加额外的一条拷贝约束:让 变量的位置 与 第四行的输出 交换对调:

如果 Prover 是诚实的,那么对于 ,下面的算术约束等式成立:

验证协议的大概思路如下:

协议开始:Prover 如实填写 表格,然后把 表格的每一列进行编码,并进行多项式编码,并把编码后的结果发送给 Verifier

协议验证阶段:Verifier 与 Prover 通过进一步的交互,验证下面的等式是否成立:

当然这个验证还不够,还要验证 之间的关系。还有,Verifier 如何通过多项式来验证电路的运算,请看后续章节。

参考文献

  • [BG12] Bayer, Stephanie, and Jens Groth. "Efficient zero-knowledge argument for correctness of a shuffle." Annual International Conference on the Theory and Applications of Cryptographic Techniques. Springer, Berlin, Heidelberg, 2012.
  • [GWC19] Ariel Gabizon, Zachary J. Williamson, and Oana Ciobotaru. "Plonk: Permutations over lagrange-bases for oecumenical noninteractive arguments of knowledge." Cryptology ePrint Archive (2019).

理解 PLONK(二):多项式编码

在上篇文章里,我们可以把电路的计算的「合法性检查」转换成一组加法/乘法约束。假如总共有 N 个约束,那么Prover 可以通过多项式编码的方式把多个约束压缩成一个约束,让 Verifier 轻松检查。

多项式的概率检查

把多个约束验证合并的神奇能力来自于「多项式随机挑战」。如果有两个多项式 同为两个次数不超过 的多项式。那么 Verifier 只需要给出一个随机挑战值 ,计算 是否等于 即可大概率得知 ,其中出错的概率 。只要保证 足够大,那么检查出错的概率就可以忽略不计。

这个原理被称为 Schwartz-Zippel 定理。

假如要验证两个向量 是否等于 ,为了可以一步挑战验证,我们要先把三个向量编码成多项式。

一种最直接的方案是把向量当作多项式的「系数」进行编码

显然,如果 ,那么 。然后我们可以通过挑战一个随机数 来检验三个多项式在 处的取值,验证:

如果上式成立,那么

Lagrange 插值 与 Evaluation Form

假如我们要验证 ,用系数编码的方式就不容易处理了,因为 会产生很多的交叉项。并且 的项并不对应到 的系数,比如 的系数出现在 上,但同时 项的系数组成还有 。而 的系数。

我们需要另一种多项式编码方案,利用 Lagrange Basis。如果我们要构造多项式 ,使得它在定义域 上的取值为 ,即

插值需要用到一组插值多项式: ,其中 ,并且 。然后 可以按如下方式编码:

可以简单心算一下,当 时,等式右边除了第一项之外,其他项都等于零,于是 。看起来 像是一个选择器,这组多项式又被称为 Lagrange Polynomials。

我们用同样的方法来编码

如果 成立,那么 。如果 ,那么

我们现在已经把两个向量的按位乘积问题转换到了三个多项式之间的关系,接下来的问题是如何进行随机挑战验证。

我们发现:如果直接让 Verifier 发送随机数 挑战上面的等式,那么 只能属于 。如果只存在一个 使得 ,那么 Verifier 的一次挑战能发现这个错误的概率只有 ,这样 Verifier 需要挑战多次才能缩小检测出错的概率。不过这样不满足我们的要求,我们希望只通过一次挑战来检测出 Prover 的作弊行为。

我们可以把上面的等式的 取值范围去除,换成下面的等式:

这个等式在整个 定义域上都成立。这是为何?

首先我们看等式左边的多项式: ,不妨定义为 。我们可以看到 上等于零,那么意味着 恰好是 的「根集合」。于是 可以按照下面的方式进行因式分解:

换个说法, 可以被多项式 整除,并得到一个商多项式 。零多项式 又被称为 Vanishing Polynomial。

如果我们让 Prover 计算出这个 ,并且发送给 Verifier,又因为 是已知的系统参数,Verifier 可以自行计算 ,那么 Verifier 只需要一次随机检测即可判断 是否在 处等零。

进一步,如果我们使用多项式承诺(Polynomial Commitment),Verifier 可以让 Prover 来帮忙计算这些多项式在 处的取值,发送并证明这些值的正确性,这样能最大限度地减少 Verifier 的工作量。

但是, Verifier 计算 需要 的计算量。

那能否让 Verifier 继续减少工作量?答案是可以的,只要我们选择特殊的

单位根 Roots of Unity

如果我们选择单位根作为 ,那么 的计算量会降为

对于任何有限域 ,其中阶数 为素数。那么去除零之后剩下的元素构成了乘法群 ,阶数为 。由于 一定为偶数,那么 的乘法因子中一定包含若干个 ,假设记为 。那么 一定包含一个阶数为 的乘法子群。不妨设 ,那么一定存在一个阶数为 的乘法子群,记为 。 该乘法子群必然含有一个生成元,记为 ,并且 。这相当于把 次方根,因此被称为单位根。不过单位根不只有一个 ,我们会发现 都满足单位根的特性,即 。那么所有这些由 产生的单位根就组成了乘法子群

这些元素满足一定的对称性:比如 。又比如把所有的单位根求和,我们会得到零:

举一个简单的例子,我们可以在 中找到一个阶数为

其中乘法群的生成元为 。由于 13-1=3*2*2,所以存在一个阶数为 的乘法子群,其生成元为

在实际应用中,我们会选择一个较大的有限域,它能有一个较大的 Powers-of-2 乘法子群。比如椭圆曲线 BN254 的 Scalar Field,含有一个阶数为 的乘法子群,BLS-12-381 的Scalar Field 含有一个阶数为 的乘法子群。

在乘法子群 上,具有下面的性质:

我们可以进行简单的推导,假设 ,由于 的对称性,这个计算过程可以不断化简:

Lagrange Basis

对于 Lagrange 多项式, ,并且 。接下来,我们给出 的构造。

为了构造 ,先构造不等于零的多项式部分。由于 ,因此他一定包含 这个多项式因子。但该因子显然在 处可能不等于 ,即可能 。然后,我们只要让该因子除以这个可能不等于 的值即可,于是 定义如下:

不难发现, 处等于 ,其它位置 处等于

对于任意次数小于 的多项式 ,那么它都可以唯一地表示为:

我们可以用多项式在 上的值 来表示 。这被称为 多项式的求值形式(Evaluation Form),区别于系数形式(Coefficient Form)。

两种形式可以在 上可以通过 (Inverse) Fast Fourier Transform 算法来回转换,计算复杂度为

多项式的约束

利用 Lagrange Basis 我们可以方便地对各种向量计算进行约束。

比如我们想约束 向量的第一个元素为 。那么我们可以对这个向量进行编码,得到 ,并且进行如下约束:

Verifier 可以挑战验证下面的多项式等式:

再比如,我们想约束 向量的第一个元素为 ,最后一个元素为 ,其它元素任意。那么 应该满足下面两个约束。

那么通过 Verifier 给一个随机挑战数( ),上面两个约束可以合并为一个多项式约束:

接下来,Verifier 只要挑战下面的多项式等式即可:

如果想验证 两个等长向量除第一个元素之外,其它元素都相等,那要如何约束呢?假设 为两个向量的多项式编码,那么它们应该满足:

时,左边多项式的第一个因子等于零,而 时,则左边第二因子等于零,即表达了除第一项可以不等之外,其它点取值都必须相等。

可以看出,采用 Lagrange 多项式,我们可以灵活地约束多个向量之间的关系,并且可以把多个约束合并在一起,让 Verifier 仅通过很少的随机挑战就可验证多个向量约束。

Coset

在素数有限域的乘法群中,对于每一个乘法子群 ,都有多个等长的陪集(Coset),这些 Coset 具有和 类似的性质,在 Plonk 中也会用到 Coset 的概念,这里只做部分性质的介绍。

还拿 为例,我们取 ,并且乘法群的生成元 。于是我们可以得到下面两个 Coset:

\begin{split} H_1 &= g\cdot H = (g, g\omega, g\omega^2, g\omega^3) &= (2,10,11,3) \ H_2 &= g^2\cdot H = (g^2, g^2\omega, g^2\omega^2, g^2\omega^3) &= (4,7,9,6) \ \end{split}

可以看到 ,并且它们交集为空,没有任何重叠。并且它们的 Vanishing Polynomial 也可以快速计算:

References

  • Schwartz–Zippel lemma. https://en.wikipedia.org/wiki/Schwartz%E2%80%93Zippel_lemma

理解 PLONK(三):置换证明

Plonkish 电路编码用两个矩阵 描述电路的空白结构,其中 为运算开关, 为置换关系,用来约束 矩阵中的某些位置必须被填入相等的值。本文重点讲解置换证明(Permutation Argument)的原理。

回顾拷贝关系

回顾一下 Plonkish 的 表格,总共有三列,行数按照 对齐。

我们想约束 Prover 在填写 表时,满足下面的拷贝关系: ,换句话说, 位置上的值需要被拷贝到 处,而 位置上的值需要被拷贝到 处, 位置上的值被拷贝到 处。

问题的挑战性在于,Verifier 要仅通过一次随机挑战就能完成 表格中多个拷贝关系的证明,并且在看不到 表格的情况下。

Plonk 的「拷贝约束」是通过「置换证明」(Permutation Argument)来实现,即把表格中需要约束相等的那些值进行循环换位,然后证明换位后的表格和原来的表格完全相等。

简化一下问题:如何证明两个等长向量 满足一个已知的置换 ,并且

举一个例子,假设 ,即他们满足一个「左移循环换位」的置换关系,那么 。如何能证明 ,那么两个向量对应位置的值都应该相等,

\begin{array}{c{|}c|c|c|c|c} \vec{a} & a_0 & a_1 & a_2 & a_3 \ \hline \vec{a}' & a_1 & a_2 & a_3 & a_0 \ \end{array}

那么 ,于是可以得出结论: ,即 中的全部元素都相等。

对于 ,我们只需要针对那些需要相等的位置进行循环换位,然后让 Prover 证明 和经过循环换位后的 表格相等,那么可实现拷贝约束。证明两个表格相等,这个可以通过多项式编码,然后进行概率检验的方式完成。剩下的工作就是如何让 Prover 证明 确实是(诚实地)按照事先约定的方式进行循环移位。

那么接下来就是理解如何让 Prover 证明两个向量之间满足某一个「置换关系」。 置换证明(Permutation Argument)是 Plonk 协议中的核心部分,为了解释它的工作原理,我们先从一个基础协议开始——连乘证明(Grand Product Argument)。

冷启动:Grand Product

假设我们要证明下面的「连乘关系」 :

我们在上一篇文章介绍了如何证明一组「单乘法」,通过多项式编码,把多个单乘法压缩成单次乘法的验证。

这里对付连乘的基本思路是:让 Prover 利用一组单乘的证明来实现多个数的连乘证明,然后再通过多项式的编码,交给 Verifier 进行概率检查。

强调下:思路中的关键点是如何把一个连乘计算转换成多次的单乘计算。

我们需要通过引入一个「辅助向量」,把「连乘」的计算看成是一步步的单乘计算,然后辅助向量表示每次单乘之后的「中间值」:

上面表格表述了连乘过程的计算轨迹(Trace),每一行代表一次单乘,顺序从上往下计算,最后一行计算出最终的结果。

表格的最左列为要进行连乘的向量 ,中间列 为引入的辅助变量,记录每次「单乘之前」的中间值,最右列表示每次「单乘之后」的中间值。

不难发现,「中间列」向量 向上挪一行与「最右列」几乎一致,除了最后一个元素。该向量的第一个元素用了常数 作为计算初始值,「最右列」最后一个向量元素为计算结果。

向量 是一个 Accumulator,即记录连乘计算过程中的每一个中间结果:

那么显然我们可以得到下面的递归式:

于是,表格的三列编码后的多项式也将满足下面三个约束。第一个是初始值为

第二个约束为递归的乘法关系:

第三个约束最后结果

我们可以用一个小技巧来简化上面的三个约束。我们把计算连乘的表格添加一行,令 (注意: 向量的连乘积)

这样一来, 。最右列恰好是 的循环移位。并且上面表格的每一行都满足「乘法关系」!于是,我们可以用下面的多项式约束来表示递归的连乘:

接下来,Verifier 可以挑战下面的多项式等式:

其中 是用来聚合多个多项式约束的随机挑战数。其中 为商多项式,

接下来,通过 Schwartz-Zippel 定理,Verifier 可以给出挑战数 来验证上述多项式等式是否成立。

到此为止,如果我们已经理解了如何证明一个向量元素的连乘,那么接下来的问题是如何利用「连乘证明」来实现「Multiset 等价证明」(Multiset Equality Argument)。

从 Grand Product 到 Multiset 等价

假设有两个向量,其中一个向量是另一个向量的乱序重排,那么如何证明它们在集合意义(注意:集合无序)上的等价呢?最直接的做法是依次枚举其中一个向量中的每个元素,并证明该元素属于另一个向量。但这个方法有个限制,就是无法处理向量中会出现两个相同元素的情况,也即不支持「多重集合」(Multiset)的判等。例如 就属于一个多重集合(Multiset),那么它显然不等于 ,也不等于

另一个直接的想法是将两个向量中的所有元素都连乘起来,然后判断两个向量的连乘值是否相等。但这个方法同样有一个严重的限制,就是向量元素必须都为素数,比如 ,但

修改下这个方法,我们假设向量 为一个多项式 的根集合,即对向量中的任何一个元素 ,都满足 。这个多项式可以定义为:

如果存在另一个多项式 等于 ,那么它们一定具有相同的根集合 。比如

那么

我们可以利用 Schwartz-Zippel 定理来进一步地检验:向 Verifier 索要一个随机数 ,那么 Prover 就可以通过下面的等式证明两个向量 在多重集合意义上等价:

还没结束,我们需要用上一节的连乘证明方案来继续完成验证,即通过构造辅助向量(作为一个累积器),把连乘转换成多个单乘来完成证明。需要注意的是,这里的两个连乘可以合并为一个连乘,即上面的连乘相等可以转换为

到这里,我们已经明白如何证明「Multiset 等价」,下一步我们将完成构造「置换证明」(Permutation Argument),用来实现协议所需的「Copy Constraints」。

从 Multiset 等价到置换证明

Multiset 等价可以被看作是一类特殊的置换证明。即两个向量 存在一个「未知」的置换关系。

而我们需要的是一个支持「已知」的特定置换关系的证明和验证。也就是对一个有序的向量进行一个「公开特定的重新排列」。

先简化下问题,假如我们想让 Prover 证明两个向量满足一个奇偶位互换的置换:

我们仍然采用「多项式编码」的方式把上面两个向量编码为两个多项式, 。思考一下,我们可以用下面的「位置向量」来表示「奇偶互换」:

我们进一步把这个位置向量和 并排放在一起:

接下来,我们要把上表的左边两列,还有右边两列分别「折叠」在一起。换句话说,我们把 视为一个元素,把 视为一个元素,这样上面表格就变成了:

容易看出,如果两个向量 满足 置换,那么,合并后的两个向量 将满足 Multiset 等价关系。

也就是说,通过把向量和位置值合并,就能够把一个「置换证明」转换成一个「多重集合等价证明」,即不用再针对某个特定的「置换关系」进行证明。

这里又出现一个问题,表格的左右两列中的元素为二元组(Pair),二元组无法作为一个「一元多项式」的根集合。

我们再使用一个技巧:再向 Verifier 索取一个随机数 ,把一个元组「折叠」成一个值:

接下来,Prover 可以对 两个向量进行 Multiset 等价证明,从而可以证明它们的置换关系。

完整的置换协议

公共输入:置换关系

秘密输入:两个向量

预处理:Prover 和 Verifier 构造

第一步:Prover 构造并发送

第二步:Verifier 发送挑战数

第三步:Prover 构造辅助向量 ,构造多项式 并发送

第四步:Verifier 发送挑战数

第五步:Prover 构造 ,并发送

第六步:Verifier 向 查询这三个多项式在 处的取值 ,得到 ;向 查询 两个位置处的取值,即 ;向 这两个多项式发送求值查询 ,得到 ;Verifier 自行计算

验证步:Verifier 验证

协议完毕。

References:

  • [WIP] Copy constraint for arbitrary number of wires. https://hackmd.io/CfFCbA0TTJ6X08vHg0-9_g
  • Alin Tomescu. Feist-Khovratovich technique for computing KZG proofs fast. https://alinush.github.io/2021/06/17/Feist-Khovratovich-technique-for-computing-KZG-proofs-fast.html#fn:FK20
  • Ariel Gabizon. Multiset checks in PLONK and Plookup. https://hackmd.io/@arielg/ByFgSDA7D

理解 PLONK(四):算术约束与拷贝约束

回顾置换证明

上一节,我们讨论了如何让 Prover 证明两个长度为 的向量 满足一个实现约定(公开)的置换关系 ,即

基本思路是向 Verifier 要一个随机数 ,把两个「原始向量」和他们的「位置向量」进行合体,产生出两个新的向量,记为

第二步是再向 Verifier 要一个随机数 ,通过连乘的方法来编码 的 Multiset,记为

第三步是让 Prover 证明 ,即

证明这个连乘,需要引入一个辅助向量 ,记录每次乘法运算的中间结果:

由于 ,而且 ,因此我们可以用 来编码 ,从而把置换证明转换成关于 的关系证明。

最后 Verifier 发送挑战数 ,得到 然后检查它们之间的关系。

向量的拷贝约束

所谓拷贝约束 Copy Constraints,是说在一个向量中,我们希望能证明多个不同位置上的向量元素相等。我们先从一个简单例子开始:

假设为了让 Prover 证明 ,我们可以把 对调位置,这样形成一个「置换关系」,如果我们用 记录被置换向量的元素位置,那么我们把置换后的位置向量记为 ,而 为表示按照 置换后的向量

显然,只要 Prover 可以证明置换前后的两个向量相等, ,那么我们就可以得出结论:

这个方法可以推广到证明一个向量中有多个元素相等。比如要证明 中的前三个元素都相等,我们只需要构造一个置换,即针对这三个元素的循环右移:

那么根据 容易得出

多个向量间的拷贝约束

对于 Plonk 协议,拷贝约束需要横跨 表格的所有列,而协议要求 Prover 要针对每一列向量进行多项式编码。我们需要对置换证明进行扩展,从而支持横跨多个向量的元素等价。

回忆比如针对上面电路的 表格:

看上面的表格,我们要求

支持跨向量置换的直接方案是引入多个对应的置换向量,比如上表的三列向量用三个置换向量统一进行位置编码:

置换后的向量为

Prover 用一个随机数 (Verifier 提供)来合并 ,还有置换后的向量: 。然后再通过一个随机数 (Verifier 提供)和连乘来得到 的 Multisets,

又因为拷贝约束要求置换后的向量与原始向量相等,因此

如果我们用多项式对 编码,得到 ,于是 满足下面的约束关系:

如果两个 Multiset 相等 ,那么下面的等式成立:

上面的等式稍加变形,可得

我们进一步构造一个辅助的累加器向量 ,表示连乘计算的一系列中间过程

其中 的初始值为 ,Prover 按照下表计算出

如果 能与 连乘等价的话,那么最后一行 正好等于 ,即

而又因为 。这恰好使我们可以把 完整地编码在乘法子群 上。因此如果它满足下面两个多项式约束,我们就能根据数学归纳法得出 ,这是我们最终想要的「拷贝约束」:

置换关系

在构造拷贝约束前,置换关系 需要提前公开共识。表格 含有所有算术门的输入输出,但是并没有描述门和门之间是否通过引线相连,而置换关系 实际上正是补充描述了哪些算术门之间的连接关系。

因此,对于一个处于「空白态」的电路,通过 两个表格描述,其中 由选择子向量构成,而 则由「置换向量」构成。

下面是 表格

下面是 表格,描述了哪些位置做了置换

处理 Public Inputs

假如在上面给出的小电路中,要证明存在一个 Assignment,使得 out 的输入为一个特定的公开值,比如 。最简单的办法是使用 表中的 列,并增加一行约束,使得 ,因此满足下面等式

但这个方案的问题是:这些公开值输入输出值被固定成了常数,如果公开值变化,那么 多项式需要重新计算。如果整体上 表格的行数比较大,那么这个重新计算过程会带来很多的性能损失。

能否在表格中引入参数,以区分电路中的常数列?并且要求参数的变化并不影响其它电路的部分?这就需要再引入一个新的列,专门存放公开参数,记为 ,因此,算术约束会变为:

我们还可以通过修改拷贝约束的方式引入公开参数。

[!TODO]

位置向量的优化

我们上面在构造三个 向量时,直接采用的自然数 ,这样在协议开始前,Verifier 需要构造 3 个多项式 ,并且在协议最后一步查询 Oracle,获得三个多项式在挑战点 处的取值

思考一下, 向量只需要用一些互不相等的值来标记置换即可,不一定要采用递增的自然数。如果我们采用 的话,那么多项式 会被大大简化:

其中 为互相不等的二次非剩余。

这样一来,这三个多项式被大大简化,它们在 处的计算轻而易举,可以直接由 Verifier 完成。

这个小优化手段最早由 Vitalik 提出。采用 是为了产生 的陪集(Coset),并保证 Coset 之间没有任何交集。我们前面提到 的乘法子群,如果 存在交集,那么 。这个论断可以简单证明如下:如果它们存在交集,那么 ,于是 ,又因为 ,那么 ,那么 ,那么 ,同理可得 ,于是

如果 的列数更多,那么我们需要选择多个 来产生不相交的 Coset。一种最直接的办法是采用 ,其中 为乘法子群 的生成元,

协议框架

预处理:Prover 和 Verifier 构造

第一步:Prover 针对 表格的每一列,构造 使得

第二步: Verifier 发送随机数

第三步:Prover 构造 ,使得

第四步:Verifier 发送随机挑战数

第五步:Prover 计算 ,并构造商多项式

其中

其中商多项式

第六步:Verifier 发送随机挑战数 ,查询上述的所有 Oracle,得到

Verifier 还要自行计算

验证步:

参考文献

理解 Plonk(五):多项式承诺

什么是多项式承诺

所谓承诺,是对消息「锁定」,得到一个锁定值。这个值被称为对象的「承诺」。

这个值和原对象存在两个关系,即 Hiding 与 Binding。

Hiding: 不暴露任何关于 的信息;

Binding:难以找到一个 ,使得

最简单的承诺操作就是 Hash 运算。请注意这里的 Hash 运算需要具备密码学安全强度,比如 SHA256, Keccak 等。除了 Hash 算法之外,还有 Pedersen 承诺等。

顾名思义,多项式承诺可以理解为「多项式」的「承诺」。如果我们把一个多项式表达成如下的公式,

那么我们可以用所有系数构成的向量来唯一标识多项式

如何对一个多项式进行承诺?很容易能想到,我们可以把「系数向量」进行 Hash 运算,得到一个数值,就能建立与这个多项式之间唯一的绑定关系。

或者,我们也可以使用 Petersen 承诺,通过一组随机选择的基,来计算一个 ECC 点:

如果在 Prover 承诺多项式之后,Verifier 可以根据这个承诺,对被锁定的多项式进行求值,并希望 Prover 可以证明求值的正确性。假设 ,Verifier 可以向提供承诺的 Prover 询问多项式在 处的取值。Prover 除了回复一个计算结果之外(如 ) ,还能提供一个证明 ,证明 所对应的多项式 处的取值 的正确性。

多项式承诺的这个「携带证明的求值」特性非常有用,它可以被看成是一种轻量级的「可验证计算」。即 Verifier 需要把多项式 的运算代理给一个远程的机器(Prover),然后验证计算(计算量要小于直接计算 )结果 的正确性;多项式承诺还能用来证明秘密数据(来自Prover)的性质,比如满足某个多项式,Prover 可以在不泄漏隐私的情况下向 Verifier 证明这个性质。

虽然这种可验证计算只是局限在多项式运算上,而非通用计算。但通用计算可以通过各种方式转换成多项式计算,从而依托多项式承诺来最终实现通用的可验证计算。

按上面 的方式对多项式的系数进行 Pedersen 承诺,我们仍然可以利用 Bulletproof-IPA 协议来实现求值证明,进而实现另一种多项式承诺方案。此外,还有 KZG10 方案,FRI,Dark,Dory 等等其它方案。

KZG10 构造

与 Pedersen 承诺中用的随机基向量相比,KZG10 多项式承诺需要用一组具有内部代数结构的基向量来代替。

请注意,这里的 是一个可信第三方提供的随机数,也被称为 Trapdoor,需要在第三方完成 Setup 后被彻底删除。它既不能让 Verifier 知道,也不能让 Prover 知道。当 设置好之后, 被埋入了基向量中。这样一来,从外部看,这组基向量与随机基向量难以被区分。其中 ,而 ,并且存在双线性映射

对于一个多项式 进行 KZG10 承诺,也是对其系数向量进行承诺:

这样承诺 巧好等于

对于双线性群,我们下面使用 Groth 发明的符号 表示两个群上的生成元,这样 KZG10 的系统参数(也被称为 SRS, Structured Reference String)可以表示如下:

下面构造一个 的 Open 证明。根据多项式余数定理,我们可以得到下面的等式:

这个等式可以解释为,任何一个多项式都可以除以另一个多项式,得到一个商多项式加上一个余数多项式。由于多项式在 处的取值为 ,那么我们可以确定:余数多项式一定为 ,因为等式右边的第一项在 处取值为零。所以,如果 ,我们可以断定: 处等零,所以 的根,于是 一定可以被 这个不可约多项式整除,即一定存在一个商多项式 ,满足上述等式。

而 Prover 则可以提供 多项式的承诺,记为 ,作为 的证明,Verifier 可以检查 是否满足整除性来验证证明。因为如果 ,那么 则无法被 整除,即使 Prover 提供的承诺将无法通过整除性检查:

承诺 是群 上的一个元素,通过承诺的加法同态映射关系,以及双线性映射关系 ,Verifier 可以在 上验证整除性关系:

q(X),[χ]2ζ[1]2)

有时为了减少 Verifier 在 上的昂贵操作,上面的验证等式可以变形为:

q(X)y[1]1, [1]2)=?e(C_q(X), [χ]2)

同点 Open 的证明聚合

在一个更大的安全协议中,假如同时使用多个多项式承诺,那么他们的 Open 操作可以合并在一起完成。即把多个多项式先合并成一个更大的多项式,然后仅通过 Open 一点,来完成对原始多项式的批量验证。

假设我们有多个多项式, ,Prover 要同时向 Verifier 证明 ,那么有

通过一个随机数 ,Prover 可以把两个多项式 折叠在一起,得到一个临时的多项式

进而我们可以根据多项式余数定理,推导验证下面的等式:

我们把等号右边的第二项看作为「商多项式」,记为

假如 处的求值证明为 ,而 处的求值证明为 ,那么根据群加法的同态性,Prover 可以得到商多项式 的承诺:

因此,只要 Verifier 发给 Prover 一个额外的随机数 ,双方就可以把两个(甚至多个)多项式承诺折叠成一个多项式承诺

并用这个折叠后的 来验证多个多项式在一个点处的运算取值:

从而把多个求值证明相应地折叠成一个,Verifier 可以一次验证完毕:

由于引入了随机数 ,因此多项式的合并不会影响承诺的绑定关系(Schwartz-Zippel 定理)。

协议:

公共输入: f2=[f2(χ)]1

私有输入:

证明目标:

第一轮:Verifier 提出挑战数

第二轮:Prover 计算 ,并发送

第三轮:Verifier 计算

多项式约束与线性化

假设 分别是 的 KZG10 承诺,如果 Verifier 要验证下面的多项式约束:

那么 Verifier 只需要把前两者的承诺相加,然后判断是否等于 即可

如果 Verifier 需要验证的多项式关系涉及到乘法,比如:

最直接的方法是利用双线性群的特性,在 上检查乘法关系,即验证下面的等式:

但是如果 Verifier 只有 上的承诺 ,而非是在 上的承诺 ,那么Verifer 就无法利用双线性配对操作来完成乘法检验。

另一个直接的方案是把三个多项式在同一个挑战点 上打开,然后验证打开值之间的关系是否满足乘法约束:

同时 Prover 还要提供三个多项式求值的证明 供 Verifier 验证。

这个方案的优势在于多项式的约束关系可以更加复杂和灵活,比如验证下面的稍微复杂些的多项式约束:

假设 Verifier 已拥有这些多项式的 KZG10 承诺, 。最直接粗暴的方案是让 Prover 在挑战点 处打开这 6 个承诺,发送 6 个 Open 值和对应的求值证明:

Verifier 验证 个求值证明,并且验证多项式约束:

我们可以进一步优化,比如考虑对于 这样一个简单的多项式约束,Prover 可以减少 Open 的数量。比如 Prover 先 Open ,发送求值证明 然后引入一个辅助多项式 ,再 Open 处的取值。

显然对于一个诚实的 Prover, 求值应该等于零。对于 Verifier,它在收到 之后,就可以利用承诺的加法同态性,直接构造 的承诺:

这样一来,Verifier 就不需要单独让 Prover 发送 的 Opening,也不需要发送新多项式 的承诺。Verifier 然后就可以验证 这个多项式约束关系:

这个优化过后的方案,Prover 只需要 Open 两次。第一个 Opening 为 ,第二个 Opening 为 。而后者是个常数,不需要发送给 Verifier。Prover 只需要发送两个求值证明,不过我们仍然可以用上一节提供的聚合证明的方法,通过一个挑战数 ,Prover 可以聚合两个多项式承诺,然后仅需要发送一个求值证明。

我们下面尝试优化下 个多项式的约束关系的协议:

协议:

公共输入: f2=[f2(χ)]1h2=[h2(χ)]1g=[g(χ)]1

私有输入:

证明目标:

第一轮:Verifier 发送

第二轮:Prover 计算并发送三个Opening,

第三轮:Verifier 发送 随机数

第四轮:Prover 计算 ,利用 折叠 这四个承诺,并计算商多项式 ,发送其承诺 作为折叠后的多项式在 处的求值证明

第五轮:Verifier 计算辅助多项式 的承诺

计算折叠后的多项式的承诺:

计算折叠后的多项式在 处的求值:

检查下面的验证等式:

这个优化后的协议,Prover 仅需要发送三个 Opening,一个求值证明;相比原始方案的 6 个 Opening和 6 个求值证明,大大减小了通信量(即证明大小)。

Reference

理解 Plonk(六):实现 Zero Knowledge

在前文的 Plonk 协议中,所有的多项式承诺都没有混入额外的随机数进行保护,因此当一个未被随机化的多 项式承诺 经过一次或者多次 Open,会泄露 自身的信息,这会限制协议在需要隐私保护的 场景中应用。

考虑一个 次多项式 ,只要它在四个不同的点上 Open ,多项式就可以通过 Lagrange 插值来复原。 然而即使一个次数超过一百万的多项式,哪怕被打开一次也会泄漏关于原多项式的部分信息。

为了实现 Zero Knowledge 性质的 Plonk,我们需要在多项式中加入足够多的随机因子,确保在多项式 打开 次之后,仍然不会泄漏原多项式的信息,保证没有知识泄漏。

Plonk 协议的大致流程为:Prover 构造多项式,然后发送多项式的承诺给 Verifier。然后 Verfier 挑战两个随机挑战点 ,其中 为 子群 的生成元。下面是 Prover 需要构造的多项式列表:

  • Witness 多项式:
  • 置换累乘多项式:
  • 商多项式:

其中三个 Witness 多项式要在 这一个点处打开,置换累乘多项式 要在 两个点处打开,而三个商多项式则不需要被打开。

Prover 要混入两类随机因子,第一类是保护承诺本身,满足信息隐藏 Hiding,一个承诺一般只需要混入一个随机数即可; 第二类是保护多项式承诺在打开之后仍然保证原多项式信息不会泄漏。如果多项式打开的次数越多(假设每次打开的位置都不同), Prover 就要混入越多的随机因子。

第一类的随机因子,也可以用多项式承诺方案来实现,比如 Bulletproof-IPA,或者 KZG10-with-Hiding,这些多项式承诺方案本身已经支持 Hiding 。如果 Plonk 后端采用的是朴素的 KZG10,那么就需要在 Plonk 协议层面增加足够的随机因子,不仅保证承诺自身的 Hiding 性质,还要保护承诺的打开。

下面我们介绍两个不同的混入随机因子方案实现 Zero Knowledge 的方法。第一个方法比较经典,是为多项式加上一个盲化(Blinding)用途的多项式,GWC19 论文[3](或其它学术论文)中正是采用的这种方法。而第二个方法是在向量的对齐填充空间里面填入随机数,再插值产生多项式的,这是工程实现中的常见方法。

方法一:Blinding 多项式

我们先看 Witness 多项式 ,它是由下面的等式计算:

我们假设 ,其中

在 Plonk 协议中,Prover 需要计算 的取值,其中 为 Verifier 给出的随机挑战点。

如果我们直接鲁莽地在 中混入随机数 ,比如 ,那么 可能就不再满足算术约束:

而且也无法满足置换约束。

如果要让随机化后的多项式 满足「算术约束」和「置换约束」,那么我们可以考虑在乘法子群 之外增加一些随机的点,这样可以让随机化后的多项式 整个乘法子群上的取值仍然与 完全相等,但是整个多项式却已经被随机化了。所谓的在 上的取值相等,就是保证随机化后的多项式仍然可以被 整除。下面是随机化多项式的构造:

这里 为 Blinding 多项式,包含两个随机因子 ,它们恰好是自变量的不同次数的系数,这样可以保证线性不相关。换个方式理解,只有对这个 Blinding 多项式打开两次以上,才可以计算出所有的随机因子。如果只打开一次,Blinding 多项式会被消耗掉一个随机因子,还剩下一个起作用的随机因子。

简单检查下,我们可以发现新定义的 符合要求,能满足算术约束。同时因为 ,因此 也一定满足置换关系。

这里 被混入了两个随机因子,其中一个随机因子可以保护 被打开一次,另一个随机因子用来实现承诺 本身的信息隐藏。

考虑下置换累乘多项式 ,假如多项式承诺 被打开两次的话,那么就需要混入三个随机因子,构造一个次数为 的 Blinder 多项式, ,然后混入到 中:

最后考虑商多项式 ,由于他们不需要在任何点打开,因此只要加上随机因子即可,不过这几个商多项式有额外的要求,即他们三个需要一起能拼出真正的商多项式

我们可以采用下面的方式,为每一个多项式分片混入一个随机因子,并且保证他们拼起来之后仍然等于

low(X)+b0XN=tmid(X)b0+b1XN=thigh(X)b1

容易检验:

同理,如果 的次数达到了 ,那么就需要三个随机数给四个 分段加上随机数,实现 Hiding。

这个方法存在一个问题,就是 Blinding 多项式的次数会超过 ,这里 。因为 的次数为 ,因此 次数为 。如果 Plonk 后端采用的是 Bulletproof-IPA 这类的多项式承诺,承诺会要求多项式的次数按 对齐,这样盲化之后的多项式的次数刚刚超出 ,只能对齐到 。一些 Plonk 变种协议可能会把 Witness table 的列数增加,稍稍超出的多项式次数会使 的计算在一个更大的子群上完成。

方法二:随机因子对齐

下面介绍的第二种方法不会推高多项式的次数。考虑到 子群的大小 是按 对齐,在实际电路中,一般情况下需要把 Witness Table 的长度对齐到 ,为了对齐,需要把空余的空间用零填满。

那么这里可以用随机数来代替零填充对齐空间,好处是这些随机数可以保护表中的其它正常数据。

Daniel Lubarov 按照这个思路给出了第二种随机数填充实现 Zero-Knowledge 性质的办法[1]。

对于商多项式,因为方法一不会推高他们的次数,因此我们下面只考虑剩下的两类多项式:

  • Witness 多项式:
  • 置换累乘多项式:

先看第一类多项式,以 为例,它编码了 向量。如果本身向量长度不足 ,一般情况下是用零补齐,我们现在可以考虑让 Prover 额外用两个随机数补齐,这样做的效果和方法一的 Blinding 多项式完全一样。 如下所示:

N1(X))

其中 也可以看成是利用 Lagrange Basis 产生的 Blinding 多项式。这里假设 的长度为 为两个随机数。假设 的系数为固定值,那么当 被打开两次之后, N2(X)+b1L_N1(X) 的系数即可被求解,从而失去随机化的能力。因此, 只能承受一次安全的打开操作(假设协议基于 Non-hiding 的多项式承诺)。

对于置换累乘多项式 ,则需要在累乘向量 的尾部引入随机值。考虑下 的计算方式:

列出所有的 的计算如下:

假如我们想设置 为随机值,我们需要让 这两个元素设置一个 Copy Constraint,并填上同一个随机数 。如果 设置为零,那么

又因为

那么 的概率分布与 相同。这样我们通过把 Witness Table 的最后两行用来填入随机数 ,并且设置一个 Copy Constraint 来随机化 。如果要再引入一个随机数 ,一种方法是我们再征用 Witness table 的两行, ,可以让 随机化。或者我们节省下空间,利用 来构造一个随机数 的 Copy Constraint。同理,我们可以再用两行 来引入 。 这样,我们总共征用了四行,引入了三个随机数

最后我们推导一下 ,请注意 ,因为前面的 Permutation 项都已经消完。

于是 中各自包含了一个随机数。请注意这个方法需要在 Witness table 中留有足够的 padding 空间,并且 的盲化因子不能与 的重复,那么总共需要留出 6 排空间,并且把 盲化因子提前到第 排:

满足 Hiding 性质的 KZG10

在 Daniel Lubarov 的 Blog 中讲述的方案是基于带有 Hiding 性质的多项式承诺 IPA(Inner product argument)。因此在 中只需要混入一个随机因子, 中只混入两个随机因子。

但是我们也可以选择一个带有 Hiding 性质的 KZG10 承诺方案,这样也可以按照 Halo2 方式混入较少的随机数实现 Zero-knowledge。

这个方案参考了 Marlin 论文[2]的 Appendix B.3,基于 AGM 模型的 KZG10-with-hiding。

在 Setup 阶段,我们需要产生两倍长的 srs:

如果我们要承诺一个多项式 ,那么需要额外产生一个次数相同的 Blinder 多项式:

然后计算承诺:

如果我们要在 处打开一个多项式承诺,先计算 ,还要计算盲化多项式 的求值, ,然后产生这两个多项式的求值证明:

检查求值证明的方式如下:

我们可以看到为了实现 Hiding,计算承诺和打开承诺的成本会加倍。如果我们限定多项式只能被打开一次(或者有限次),那么我们可以采用更低次数的盲化多项式 。假如我们只考虑多项式最多被打开一次的情况,那么 只需要是一个一次多项式,同时也可以减少 srs 的尺寸。

最后请注意的是,仅有实现 Hiding 的多项式承诺不足以实现 Plonk 的 Zero-knowledge,仍然需要在 Plonk 协议层面混入足够的随机的盲化因子。

参考文献

  • [1] Adding zero knowledge to Plonk-Halo https://mirprotocol.org/blog/Adding-zero-knowledge-to-Plonk-Halo
  • [2] Chiesa, Alessandro, Yuncong Hu, Mary Maller, Pratyush Mishra, Noah Vesely, and Nicholas Ward. "Marlin: Preprocessing zkSNARKs with universal and updatable SRS." In Advances in Cryptology–EUROCRYPT 2020: 39th Annual International Conference on the Theory and Applications of Cryptographic Techniques, Zagreb, Croatia, May 10–14, 2020, Proceedings, Part I 39, pp. 738-768. Springer International Publishing, 2020. https://eprint.iacr.org/2019/1047.
  • [3] Gabizon, Ariel, Zachary J. Williamson, and Oana Ciobotaru. "Plonk: Permutations over lagrange-bases for oecumenical noninteractive arguments of knowledge." Cryptology ePrint Archive (2019).

理解 PLONK(七):Lookup Gate

传统上我们通过编写算术电路来表达逻辑或者计算。而算术电路只有两种基本门:「加法门」与「乘法门」。当然通过组合,我们可以基于加法和乘法构造复杂一点的元件(Gadget)来复用,但是在电路处理过程中,这些 Gadget 还是会被展开成加法门和乘法门的组合。

自然我们想问:能否使用除加法和乘法之外的「新计算门」?

Plonk 相关的工作给出了一个令人兴奋的扩展:我们有能力构造出更复杂些的基本计算单元。如果一个计算的输入和输出满足一个预先设定的多项式的话,那么这个计算可以作为基本计算单元,这个改进被称为 「Custom Gate」,实际上你可以理解为这是一种多输入的「多项式门」。

故事还没有结束,论文 GW20 又给出了一个制造「Lookup Gate」的方法。这个门的输入输出没有必要局限于多项式关系,而是可以表达「任意的预定义关系」。What? 任意的关系?是的,你没听错,尽管这有点令人难以置信。

思路不难理解:如果我们在电路之外预设一个表格,表中每一行表示特定计算的输入输出关系,例如:

in1in2in3out
1234
5678
1159

这个表格就代表一个 Lookup 门的定义。如果你问我这个门究竟表达了什么计算,我无法回答(乱写的)。不过只要能给出这样一张表格,我们就可以在电路里面接入一个门,它的输入输出关系「存在于表中的某一行」。

这种门被称为 Lookup Gate,即查表门(或查表约束)。

如果当我们在 Plonk 电路中接入查表门,那么 Plonk 协议就要检查这个门的输入输出是否合法,然后就会去查我们实现预设的表格,看看其输入输出关系是否能在表中找到对应的一行。如果表中存在这样的条目,那么这个门就合法,否则被视为非法。

在现实应用中,最多采用查表方式的门是关于位运算。如一个 8-bit 异或运算,只需要 大小的表格即可。此外对于采用大量位运算的 SHA256算法,也可以通过建立一个 Spread Table 来大大加速各种位运算的效率。

基本思路

实现查表门的一个关键技术是 Lookup Argument 协议,即如何证明一条(或多条)记录是否存在于一个公开的表中。

可能有朋友会条件反射想到 Merkle Tree,如果我们把表格按行计算 hash,这些 hash 就可以产生一个 Merkle Root,然后通过 Merkle Path 就能证明一条记录是否存在表格中。但是这个方法(以及所有的 Vector Commitment 方案)不适合查表场景。原因有两个,一是这种方案会暴露记录在表格中的位置。假如 Prover 想隐藏记录的信息,即在查询证明不暴露位置,那么仅 Merkle Tree 就难以胜任了。理论点说,这里我们需要 Set-Membership Argument,而非 Vector-Membership Argument。第二个原因:如果有大量的记录条目(比如条目数量为 )需要查表,那么所产生的证明即 Merkle Path,可能会比较大,最坏情况是

简而言之,我们需要一种新的,并且高效的查表协议。本文介绍两个常见的查表协议,为了简化表述,我们先只考虑单列表格的查询,然后再扩展到多列表格的情况。

Halo2-lookup 方案

基于 Permutation Argument,Halo2 给出了一个简洁易懂的 Lookup Argument 方案。

假如我们有一个表格向量 ,表格中不存在相同元素。然后有一个查询向量 ,我们接下来要证明 ,请注意 中会有重复元素。

我们引入一个关键的辅助向量 ,它是 的一个重新排序(置换),使得 中的所有查询记录都按照 的顺序进行排序,比如 ,那么重排后,

可以看出, 中的重复元素被放在了一起,并且整体上按照 中元素出现的顺序。我们把 中连续重复元素的第一个标记出来:

我们再引入一个辅助向量 ,它是对 的重新排序,使得 中被标记元素可以正好对应到 中相同位置上的元素:

请注意看 ,其中被方框标记的元素和 中相同位置的方框元素值完全相同,未被标记的元素则没有出现在 中。

于是我们可以找出一个规律: 中的每一个未标记元素等于它左边的相邻元素,而每一个被标记元素等于 同位置元素,即 或者

将两个向量 与重排向量 通过 Lagrange Basis 进行多项式编码,我们得到 , , ,他们会满足下面的等式:

但上面这个等式不足以约束重排向量的可靠性。考虑如果 ,也会满足上面的等式,但是 并不是合法的查询记录。因此,我们还要加入一条约束防止出现 上 循环回卷导致的漏洞:要求 两个向量的第一个元素必须相同, 即 ,用多项式约束表达如下:

剩下的工作是证明 满足某一个「置换」关系,且 也满足某个「置换」关系。由于,这两个置换关系只不需要约束具体的置换向量,因此我们可以直接采用 Grand Product Argument 来约束这两个置换关系:

下面重新整理下这个协议

协议框架

公共输入:表格向量

秘密输入:查询向量

预处理:Prover 和 Verifier 构造

第一步:Prover 构造多项式并发送承诺

第二步:Verifier 发送挑战数

第三步:Prover 构造多项式并发送承诺

第四步:Verififer 发送挑战数

第五步:Prover 计算并发送商多项式

第六步:Verifier 发送挑战数

第七步:Prover 发送 ,并附带上 evaluation proofs(略去)

第八步:Verifier 验证(注意这里为了简化,去掉了KZG10的聚合优化和线性化优化)

Plookup 方案

然后我们再看看论文 GW20 给出的方案 —— Plookup。与 Halo2-lookup 相比,Plookup 可以省去 向量。

重申一下 Plookup 证明的场景:Verifier 已知表格 向量,Prover 拥有一个秘密的查询向量 ,Prover 要证明 中的每一个元素都在 中,即

方案 Plookup 只需要引入一个辅助向量 ,它被定义为 上的重排,且向量元素的排列遵照 中各个元素出现的顺序。

举例说明,假设 ,如果 ,那么 。可以看到,和 Halo2-lookup 中的 一样, 中相等的元素被排在了一起。

如果向量 满足 ,并且 ,那么就可以证明

第一个关键点是因为 中的查询记录是任意的,查询顺序并没有遵守 中的元素顺序。而通过辅助向量 ,我们就可以把 的查询记录进行重新排序,这有利于排查 中元素的合法性,确保每一个 都出现在 中。但如何保证由 Prover 构造的 是按照 的元素顺序进行排序的?Plookup 用了一个直接但巧妙的方法,考虑把 中的每一个元素和他相邻下一个元素绑在一起,然后可以构成一个新的 Multiset;同样,我们把 中的每一个元素与相邻下一个元素组成一个元组,并构成一个 Multiset;我们还要把 中的每一个元素和它自身构成一个二元组 Multiset。我们用 来表示这三个新的 Multiset,并证明它们满足一定的关系,从而保证 排序的正确性。

F{(fi,fi)}

这个方法与 Permutation Argument 的基本思想非常类似。回忆下,我们在 Permutation Argument 中,利用了 绑定元素和其位置的「二元组」的 Multiset 来保证任一个 都会出现在位置 上;通过与另一个二元组 Multiset 的相等,可以证明 满足置换函数 。比如下面这个置换函数为奇偶互换的例子:

假设两个向量 ,如果它们满足上面的 Multiset 相等关系,我们可以知 ,满足奇偶互换的关系。

另一个关键点是如何保证 中的元素都在 中出现?这个问题被归结到一个新问题,即 中那些相邻的重复元素一定来自于 ,假如 中有 个重复元素,那么我们可以要求其中第一个来自于 ,剩下的 个元素来自于 。如果 中一旦出现了一个不在 中的元素(假设为 ),那么因为 的重排,那么 中一定会出现 (假设 ),这时在 中一定会出现 这样两个元素,它们无法出现在 这个 Multiset中,也不会出现在 中。

举几个例子,假设 的长度为 , 如果 ,那么 向量在各个位置上都相等。

假设增加一条查询记录,即 ,那么 ,这时候 只有唯一的表达,

假设 为不出现在 中的元素,那么 一定没有办法塞入到 S 中,因为在 中,和 相邻的元素 。因此

假设 ,那么 也只有唯一的表达, ,同样可以检验:

更形式化一些,我们可以用数学归纳法推导:先从 为空开始推理, 。这样我们只要检查 满足 Multiset 意义上的相等,就可以满足 ,且

现在看归纳步,假设 ,如果我们在 中添加一个新元素 ,且 ,那么在 中会比 额外多一个元素 。因为 ,那么重排向量 k+1 中一定包含了相邻的两个 ,其中一个来自 ,另一个来自于 。因此,我们可以得出结论:

另一种情况, 假设 ,如果我们在 添加的新元素 ,即是一条违法查询,假设为 。那么 中存在与 相邻的两个元素, ,即 。它们构成了 中的两个异类元素 ,导致

到此为止,我们已经可以确信,通过验证 相等就可以判定 是正确的重排,并且 中的每一个元素都出现在 中。接下来我们把这个问题转换成多项式之间的约束关系。

首先 Prover 借助 Verifier 提供的挑战数 ,把 中的每一个二元组元素进行「折叠」,转换成单值。这样新约束等式为:

然后 Prover 再借助 Verifier 提供的一个挑战数 ,把上面的 Multiset Equality Argument 归结到 Grand Product Argument:

不过这里请注意的是,在 Plookup 论文方案中,并没有采用上面的证明转换形式。而是调换了 的使用顺序:

归结后的 Grand Product 约束等式为:

注:个人认为,上述两种证明转换形式没有本质上的区别。为了方便理解论文,我们后文遵从 Plookup 原论文的方式。

接下来,我们要对向量进行多项式编码,但是这里会遇到一个新问题。即 多项式的次数会超出 的次数或 的次数,特别当 的长度接近或者等于 的大小, 的次数可能超出 的大小。Plookup 的解决方式是将 拆成两半, ,但是 的最后一个元素要等于 的第一个元素:

这样做的目的是,确保能在两个向量中描述 中相邻两个元素的绑定关系。比如 ,那么 ,而 ,可以看出他们头尾相接。

这样一来, 的长度最长也只能是 ,但如果 要按照 对齐,那么 的长度就不够了(无法在长度为 的乘法子群上编码成多项式)。为了解决这个问题,Plookup 选择把 的有效长度限制在 ,所谓有效长度是指, 的实际长度为 ,但是其最后一条查询记录并不考虑其合法性。

于是 向量可以拆成两个长度为 的向量,其中一半 ,另一半

接下来 Prover 要引入 Accumulator 辅助向量 来证明 Grand Product:

1

我们仍然看下这样一个例子: ,于是 ,拆成两个头尾相接的向量: 。那么,我们可以把相邻元素构成的二元组向量写出来:

\begin{split} F &= (f_i, f_i) & = & {(2,2), (4,4), (4,4)}\ T &=(t_i, t_i) & = & {(1,2), (2,3), (3,4)}\ S^{lo} &= (s^{lo}_i, s^{lo}_i) & = & {(1,2), (2,2), (2,3)}\ S^{hi} &= (s^{hi}_i, s^{hi}_i) & = & {(3,4), (4,4), (4,4)}\ \end{split}

容易检验,他们满足下面的关系:

于是,利用一个辅助函数 ,我们定义

进行编码,我们可以得到 多项式,它应该满足下面三条约束:

此外,根据 的递推关系, 还要满足下面的约束:

总共有四条多项式约束,这里略去完整的协议。

Plonkup 的优化

在论文 Plonkup 论文中给出了一个简化方法,可以去除一个多项式约束。在 Plookup 方案中, 向量被拆分成两个向量, ,但要要求这两个向量头尾相接。

Plonkup 给出了一种新的拆分方案,即按照 的奇偶项进行拆分,拆成

注意,这里不再需要限制 的长度为 ,而是可以到 ,这样 的长度可以到 ,拆分成两个长度为 的向量,之所以可以去除这个限制,是因为 之间的关系可以在 回卷到起始位置,这样只需要要求 即可。 向量可以重新定义为:

我们可以举一个简单的例子:假设 ,于是

\begin{split} F &= (f_i, f_i) &= {(2,2),(4,4),(4,4), {\color{blue}(1,1)}} \ T &= (t_i,t_{i+1}) &= {(1,2),(2,3),(3,4),{\color{red}(4,1)}} \ S^{even} &= (s^{even}_i, s^{odd}_i) &= {{\color{blue}(1,1)}, (2,2), (3,4), (4,4)} \ S^{odd} &=(s^{odd}_i, s^{even}_{i+1}) &= {(1,2), (2,3), (4,4), {\color{red}(4,1)}} \ \end{split}

容易检验,他们满足下面的关系:

我们也可以通过定义 ,并仔细检查每一项,确认只需要约束 就可以约束 的正确性。

这里辅助函数

于是多项式 只需要满足如下两条约束:

还有

多列表格与多表格扩展

通常查询表是一个多列的表,比如一个 8bit-XOR 计算表是一个三列的表。对于 Plookup 方案与 Halo2-lookup 方案,我们直接可以通过随机挑战数来把一个多列表格折叠成一个单列表格。

假如计算表格为 ,那么相应的查询记录也应该是个三列的表格,记为 。如果 ,对所有的 都成立,那么 是一个合法的查询记录。 通过向 Verifier 要一个随机挑战数 ,我们可以把计算表格横向折叠起来:

同样,Prover 在证明过程中,也将查询记录横向折叠起来:

接下来,Prover 和 Verifier 可以利用单列表格查询协议( Plookup 协议或 Halo2-lookup 协议)完成证明过程。

如果存在多张不同的表格,那么可以给这些表格增加公开的一列,用来标记表格编号,这样可以把多表格视为增加一列的多列的单一表格。

与 Plonk 协议的整合

由于计算表格 是一个预定义的多列表格,因此它可以在 Preprocessing 阶段进行承诺计算,并把这些表格的承诺作为后续协议交互的公开输入。

在 Plonk 协议中,因为我们把表格的查询视为一种特殊的门,因此查询记录 本质上正是 的折叠。为了区分「查询门」和「算术门」,我们还需要增加一个选择向量 ,标记 Witness table 中的某一行是算术门,还是查询门。

下面我们按照 Plonkup 论文中的协议,大概描述下如何将 Lookup Argument 整合进 Plonk 协议。

预处理:Prover 和 Verifier 构造

第一步:Prover 针对 表格的每一列,构造 使得

第二步:Verifier 发送随机数 ,用以折叠表格

第三步:Prover 构造并发送 ,分别编码 ,其中 计算如下

这里请注意,当 时,表示这一行约束不是查询门,因此需要填充上一个存在 中的值,这里我们取表格的最后一个元素作为查询记录填充。

Prover 计算 ,并拆分为 ,构造并发送

第四步: Verifier 发送随机数

第五步:Prover 构造(并发送)拷贝约束累乘多项式 ,使得

其中

Prover 构造(并发送)查询累乘多项式 ,使得:

其中

第六步:Verifier 发送随机挑战数

第七步:Prover 计算 ,并构造商多项式

后续步:Verifier 发送随机挑战数 ,Prover 打开各个多项式,Verifier 自行计算 ,并验证各个多项式在 处的计算证明,并验证这些打开点满足上面等式。

完整的协议请参考Plonkup论文 [2]。

Reference

  • [1] Ariel Gabizo, Dmitry Khovratovich. flookup: Fractional decomposition-based lookups in quasi-linear time independent of table size. https://eprint.iacr.org/2022/1447.

  • [2] Luke Pearson, Joshua Fitzgerald, Héctor Masip, Marta Bellés-Muñoz, and Jose Luis Muñoz-Tapia. PlonKup: Reconciling PlonK with plookup. https://eprint.iacr.org/2022/086.

  • [3] https://zcash.github.io/halo2/design/proving-system/lookup.html

  • [4] Ariel Gabizon. Multiset checks in PLONK and Plookup. https://hackmd.io/@arielg/ByFgSDA7D

  • [5] Modified Lookup Argument (improved). https://hackmd.io/_Q8YR_JLTvefW3kK92KOFgv

Understanding and building user-facing applications with Halo2 and PLONKish proving systems

  • Circuits
  • Toolstack
  • Developer tools

halo2 proving system consists of these 3 components :

graph TD

a[arithmetisation]
b[polynomial commitment scheme]
c[accumulation scheme]

a --> b
b --> c

halo2 use :

  • [arithmetisation] : PLONKish arithmetisation
  • [polynomial commitment scheme] : inner product argument
  • [accumulation scheme] accumulation scheme
graph TD

a[PLONKish arithmetisation ]
b[inner product argument]
c[accumulation scheme]

a -- "low-degeree polynomial" --> b
b -- "commitment opening proof" --> c

PLONKish arithmetisation

![[Pasted image 20230821110734.png]]

![[Pasted image 20230821111605.png]]

  • qs : pre-processed polynomials also called selectors, generated by the . and they're basically hard-coded for your circuit
  • are witnesses by the and these have different values in each proof instance

custom gate(TurboPLONK)

all these gates are then combined with in a linearly independent way and we check that all of these gates are fulfilled.

so in this way we can move from giant primitive plonk equation to any number of gates any arbitrary linear combinations of expressions that we need.

Back to PLONK

![[Pasted image 20230821141056.png]]

cause the wire routing is baked into the trusted setup, so if you wanted a circuit that wired things differently you would need a different trusted setup.

whereas plonk is trying to avoid this and for this same trusted setup is trying to enable you to wire your circuit all kinds of ways —— using Permutation Argument.

Lookup(ultraPLONK)

problem : SHA256(hash) is expensive to do in-circuit solution : load precomputed SHA (e.g. for 8-bit values) as lookup table

![[Pasted image 20230821142653.png]]

=>

why?. ..

lookup default value when is not enabled, so that lookup argument passes on every row. (??)

the lookup argument is a more permissive version of the permutation argument. it enforces that:

every cell in a set of input columns is equal to some cell in a set of table columns

every expression in a set of input columns is equal to some expression in a set of table columns

we conceptualise the circuit as a matrix of m columns and n rows, over a given finite field

  • instance columns contain inputs shared between prover/verifier , generally used for public inputs
    • e.g. the res of SHA256
    • a root of a Merkle Tree.
  • advice columns contain private values witnessed by the prover
  • fixed columns contain preprocessed values set at key generation
    • 常量 constant
    • 查表 : Lookup table column
    • 选择子 (Selector), 同一行可以支持若干种不同的约束, 比如三元三次, 或者三元二次, 选择子就保证了, 比如说有 3 个 custom gate, 可以只满足其中一个就 OK , 或者满足其中的 2 个

Regions

what is regions ?

![[Pasted image 20230821163948.png]]

  • the instance columns are white (public inputs),
  • and the advice columns(witnessed) are pink and the
  • fixed columns(preprocessed value) are purple

![[Pasted image 20230821165358.png]]

  • the advice columns(witness) are still 
  • the  boxes just means that they have been assigned
  • the regions are fixed columns (preprocessed value)
    • : fixed column that is binary (0/1) we call these selector like in PLONK paper.
    • : non-binary so that you can witness constant values in them. for example like a fix column is five and you wanna your gate does like five times something, and we use these fixed columns.

Throughout the diagram, what are we trying to optimize?

  • we're trying to reduce the space being used .
  • Casue we do a lot of FFTs on the rows

![[Pasted image 20230821171338.png]]

What does columns do ?

  • each column, the priver needs to make a commitment to the column.
  • so, more columns, more commitments ; more commitments, larger proof.

"regions" are the boundary between gates and the global circuit layouter :

  • a block of assignments preserving relative offsets: easy to reason about how gates apply within the region
  • not affected by offsets in other regions: can be freely rearranged to optimise global space usage
  • each region is completely independent of other regions, and if you were to try to write some constraint that crossed the boundary between two regions you would quickly find that you run into problems

halo2 gadgets :

![[Pasted image 20230821191128.png]]

![[Pasted image 20230821191251.png]]

open problems / wishlist

  • DSL / intermediate representation

    • add an API to construct a Halo 2 circuit from a set of constraints (halo2#550)
    • improve connection between gate configuration and assignment (halo2#365)
  • multi-phase prover (halo2#593)

  • dynamic lookup tables (halo2#534)

  • what other features would you like to see?

multipoint opening argument

arithmetisation (cont.)

(cont.) i.e. continue, 续

  1. arithmetise statement using UltraPLONK circuit

  2. commit to polynomials encoding the main components of the circuit ;

  3. construct vanishing argument to constrain all circuit relations to zero ;

  4. evaluate the above polynomials at all necessary points ;

  5. construct the multipoint opening argument to check that all evaluations are consistent with their respective commitments ; 5.a. group commitments by the sets of points at which they were queried:

are gates , and is only every queries on the current row (at point set x ) advice columns(witness) queries on the current row () as well as the next row ()

5.b. construct polynomials to accumulate polynomials at each point set; sample(随机) to keep them linearly independent:
5.c evaluate the 's at their respective points: 5.d at each query point, interpolate the relevant qi evaluations:
5.e construct polynomials to check the correctness of qi polynomials

5.f construct , with random to keep polynomials linearly independent

5.g construct
with random challenge to keep polynomials linearly independent.
this checks that all evaluations are consistent with their respective commitments.

inner product argument

Polynomial commitment General process:

  1. : generate a setup
  2. : creates a commitment to
  3. : checks is a valid commitment to
  4. : generates an opening proof π
  5. : checks if using

Use cases

  • zkEVM: KZG
  • plonky2: fri

KZG / fri are alternatives for inner product argument


inner product argument is a type of polynomial commitment scheme .

halo2 API

![[Pasted image 20230821203602.png]]

Column data structure

#![allow(unused)]
fn main() {
/// Advice column.
Column<Advice>
/// Column for public input.
Column<Instance>
/// A fixed column for constants.
Column<Fixed>
/// A fixed column that holds binary constants.
Selector
/// A fixed column for a lookup table.
TableColumn
}

Constraint system

The constraint system is used to create columns and define custom gates.

#![allow(unused)]
fn main() {
fn advice_column(&mut self) -> Column<Advice> 
fn instance_column(&mut self) -> Column<Instance>
fn fixed_column(&mut self) -> Column<Fixed>
fn selector(&mut self) -> Selector
fn complex_selector(&mut self) -> Selector
fn lookup_table_column(&mut self) -> TableColumn
/// Enable the ability to enforce equality over cells in this column
fn enable_equality<C: Into<Column<Any>>>(&mut self, column: C)
/// Creates a new gate.
fn create_gate<C: Into<Constraint<F>>, Iter: IntoIterator<Item = C>>(
    &mut self,
    name: &'static str,
    constraints: impl FnOnce(&mut VirtualCells<'_, F>) -> Iter,
)
}
  • selector : only involving some very simple custom gate and custom constraints
  • complex_selector : you can use that to construct a lookup argument (with arbitrary times.)
  • The last 2 apis are used to set up actual constraints, such as copy constraints and custom gates. In particular, for copy constraint/permutation check, the enable_equality API must be used together with the copy_advice API (see example1.rs for more details).

Layouter

The layouter will be used during the assignment, namely(亦即) when you fill up a table with the witness. Each time you will fill up a region(每次都会填满一个 Region). You won't fill the entire table at once (你不会一下子填满整个表). layouter takes a region as input and assign values to that region.

  1. A region doesn’t need to have the same shape as custom gate, but must cover all related custom gates.

![[Pasted image 20230823145220.png]] 左边 Valid Region ✅:

  • 假设上面的 , 下面的
  • 则只开启了左边的 Custom Gate.

右边的 invalid Region ❌:

  • it didn't cover all of the cells related to the Custom Gate. and it doesn't assign all the cells you need for thatCustom Gate
  • if you turn on the selector, you should cover all of cells controlled by this Custom Gate.

2 layouters in halo2:

  1. SimpleFloorPlanner
  2. TwopassPlanner ?
SimpleFloorPlanner
  • It is a single-pass layouter. 
  • It finds the first empty row, for each column used in the region and takes a maximum.
  • trying to pack all our region as much as possible to use the fewer rows

![[Pasted image 20230823151041.png]]

  • Region 1 : use one cell for 3 advice column
  • Region 2 : "L shape"
  • Region 3 : "L shape"

Q: how does region 1 not include a selector ? A: you can think of region 1 is some private input you want to initialize it doesn't involve any check.

![[Pasted image 20230823152500.png]]

对于 Region 4 , 它本可以填到红色的 hole 里面, 但是这不是咱们 SimpleFloorPlanner 该考虑的事 ~


Chip:

The chip is not strictly necessary, but it is good to create gadgets. For more complex circuits you will have multiple chips and use them as lego blocks. A Circuit can use different chips.


Three steps to implement a circuit :

  1. Define a Config struct that includes the columns used in the circuit
  2. Define a Chip struct that configures the constraints in the circuit and provides assignment functions
  3. Define a Circuit struct that implements the Circuit trait and instantiates a circuit instance that will be fed into the prover
#![allow(unused)]
fn main() {
pub trait Circuit<F: Field> {
    type Config: Clone;
    type FloorPlanner: FloorPlanner; //we use SimpleFloorPlanner

    /// Returns a copy of this circuit with no witness values
    // without real Witness, it can still generate vk & pk..
    fn without_witnesses(&self) -> Self;

    /// The circuit is given an opportunity to describe the exact gate
    /// arrangement, column arrangement, etc.
    fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config;

    /// Given the provided `cs`(constrain system), synthesize the circuit.
    fn synthesize(&self, config: Self::Config, layouter: impl Layouter<F>) -> Result<(), Error>;
}
}
  • cs : constrain system

    • the will generate this constraint systems
  • chips are a way to make circuits modular

Why we need Region :

  • heuristically you can think of a region as like a self-contained block of flick and within this block you are concerned with relative offsets and you need certain cells to be placed relative to other cells in a specific way . whereas(相反) if you do not care about how two blocks interact with each other then you should define them in separate regions and the reason why this is better is you can hand some control to the layouter to optimize the layout of your regions
    • 启发式地来说, 您可以将一个 region 视为一个自包含的 Block,在该块内你关心相对偏移(relative offsets),并且需要以特定方式相对于其他单元放置某些单元。 而如果您不关心两个块如何相互作用,那么您应该将它们定义在 separate regions 中,原因是可以将一些控制权交给“layouter”以优化区域的布局
  • so as opposed to like using a one region for your whole circuit, you should try and break it up as far as possible into self-contained regions, unless your whole circuit is literally the same gate repeating over and over. in that case, you just need one region.
    • 因此,与为整个电路使用一个 region 相反,您应该尝试将其尽可能分解 self-contained regions ,除非整个电路实际上是一遍又一遍重复的同一个门。 在这种情况下,您只需要一个 region

Q: what's the cost model for halo2 are we trying to minimize the number of rows A:

fibonacci

GOAL : Given , we will prove

![[Pasted image 20230823230126.png]]

mkdir fibonacci
cd fibonacci
cargo init . 
code .

right now our layouter it can compress things horizontally for you, but it cannot do it vertically. the layouter has no control over like the config . so if you want two chips to reuse the same columns you have to manually specify it.

QA

Q: Is there a solidity snark verifier for halo2 ? A: Yes, you can use halo2-wrong by pse for that

Q: Do you need to supply the entire witness or can you let the circuit generate it by itself? A: You need to supply the entire witness, but, of course, you can create scripts that are able to do that. For example you can check the assign_row method inside the example1.rs file.

Q: What is the rotation doing ? A: In the example we used the rotation feature when querying values that will be used to set up custom gates. In the case of the example we are creating a custom gate that covers only one row. So we query value from the current row using cur(). If we wanted to create more complex gates we could use the next() or prev() method to query values from the next row or also access value from a specifc row by specifying the offset! 我们在 querying values (values 将用于设置 custom gates) 时使用了 rotation 功能。在本例中,我们创建一个仅覆盖一行的自定义门。因此,我们使用 cur() 从当前行查询值。如果我们想创建更复杂的门,我们可以使用 next()prev() 方法来查询下一行的值,或者通过指定偏移量来访问特定行的值!

Q: What does meta represent? A: Meta is an instance of a default constraint system . You can see it's been used inside the mock prover when calling the configure function

#![allow(unused)]
fn main() {
let mut cs = ConstraintSystem::default();
let config = ConcreteCircuit::configure(&mut cs);
}

The process look like:

key gen time

  • create a circuit instance
  • define the constraint system by calling configure
  • synthesize runs ignoring witness assignment

proving time

  • the prover receives a circuit instance
  • The prover assign the values inside the circuit by calling synthesize

Q: Where does region come from in assign_region method (file example1.rs)? A: This may sound confusing because it seems like the region is an input parameter coming out of nowhere. In order to understand it we should go back to the assign_region method defined on the Layouter trait.

The assign_region function takes an assignment as input which is a closure that takes a mutable reference to a Region instance as its argument, and returns a result of type AR. The closure is responsible for assigning variables and constraints to the Region. So, to answer the initial question, the region parameter used inside layouter.assign_region is not derived from any external source; rather, it is created by the assign_region method itself and passed to the closure as an argument. assign_region takes an assignment as input,这是一个闭包,它以一个对 Region 实例的可变引用作为其参数,并返回一个类型为 AR 的结果。这个闭包负责将变量和约束分配给 Region。因此,为了回答最初的问题,layouter.assign_region 内部使用的 region 参数并不是从任何外部来源派生出来的;相反,它是由 assign_region 方法自己创建的,并作为参数传递给闭包。

Q: When would you even set the selector to 0? A: You can set the selector to 0 when you want to skip a constraint. Also the number of rows is always , so if you are not using the entire rows you can set the selector to 0.

Q: Would it be possible to create a region that doesn't involve any selector? A: Yes, for example when you want to initalize some input values. You can create a region that doesn't involve any selector and just assign values to the table. (?)

cargo test
> running 1 test
> test fibonacci::tests::test_fib ... ok

Reference:

![[Simple-Example(halo2).excalidraw]]

Simple Example

Let's start with a simple circuit, to introduce you to the common APIs and how they are used. The circuit will take a public input c, and will prove knowledge of two private inputs  and  such that

Define instructions

Instructions are the boundary between high-level gadgets and the low-level circuit operations. Instructions may be as coarse or as granular as desired, but in practice you want to strike a balance between an instruction being large enough to effectively optimize its implementation, and small enough that it is meaningfully reusable. Instructions 介于 high-level gadgets 和底层的电路操作之间。指令既可以细粒度也可以粗粒度,但在实践中,指令的功能应当足够小,这样可以重复使用;但又要足够大,这样可以优化它的实现。设计者应当在这两者之间取得平衡

For our circuit, we will use three instructions:

  • Load a private number into the circuit.
  • Multiply two numbers.
  • Expose a number as a public input to the circuit. (将一个数设置为电路的公开输入)

We also need a type for a variable representing a number. Instruction interfaces provide associated types for their inputs and outputs, to allow the implementations to represent these in a way that makes the most sense for their optimization goals. 我们还需要一个代表数字的变量的类型。指令接口为其输入和输出提供关联类型,以允许实现以对其优化目标最有意义的方式表示这些类型

#![allow(unused)]
fn main() {
trait NumericInstructions<F: Field>: Chip<F> {
    /// Variable representing a number. 用于表示一个数的变量
    type Num;

    /// Loads a number into the circuit as a private input. 隐私输入
    fn load_private(&self, layouter: impl Layouter<F>, a: Value<F>) -> Result<Self::Num, Error>;

    /// Loads a number into the circuit as a fixed constant. 
    fn load_constant(&self, layouter: impl Layouter<F>, constant: F) -> Result<Self::Num, Error>;

    /// Returns `c = a * b`.
    fn mul(
        &self,
        layouter: impl Layouter<F>,
        a: Self::Num,
        b: Self::Num,
    ) -> Result<Self::Num, Error>;

    /// Exposes a number as a public input to the circuit.
    /// 将一个数置为电路的公开输入
    fn expose_public(
        &self,
        layouter: impl Layouter<F>,
        num: Self::Num,
        row: usize,
    ) -> Result<(), Error>;
}
}

Among them,

  • Num is used to adapt to the type handled in this interface, (适配该接口中处理的类型)
  • load_private is used to load witness,
  • load_constant is used to load constants, 
  • mul is used to calculate the multiplication of two numbers, and 
  • expose_public is used to set instance.

Define a chip implementation

定义芯片的实现 : For our circuit, we will build a chip that provides the above numeric instructions for a finite field.

If you want to develop a custom chip, you need to implement the chip trait of Halo 2.

Most of the time, using Halo 2 for circuit development does not need to define instructions and chips by oneself. But if you need to use complex ones that Halo 2 does not provide, you need to implement them yourself, such as implementing an emerging(新兴的) cryptographic algorithm.

#![allow(unused)]
fn main() {
/// 这块芯片将实现我们的指令集!芯片存储它们自己的配置,
struct FieldChip<F: Field> {
    config: FieldConfig,
    _marker: PhantomData<F>,
}
}

Every chip needs to implement the  Chip  trait. This defines the properties of the chip that a Layouter may rely on when synthesizing a circuit, as well as enabling any initial state that the chip requires to be loaded into the circuit. 每一个"芯片"类型都要实现 Chip trait , Chip trait 定义了 Layouter 在 synthesizing 电路时可能需要的关于电路的某些属性,以及若将该芯片加载到电路所需要设置的任何初始状态

synthesizing 电路 : 一般指的是类似 R1CS 那种写约束的意思

#![allow(unused)]
fn main() {
impl<F: FieldExt> Chip<F> for FieldChip<F> {
    type Config = FieldConfig;
    type Loaded = ();

    fn config(&self) -> &Self::Config {
        &self.config
    }

    fn loaded(&self) -> &Self::Loaded {
        &()
    }
}
}

Configure the chip

The chip needs to be configured with the columns, permutations, and gates that will be required to implement all of the desired instructions. (需要为芯片配置好实现我们想要的功能所需要的那些列、置换、门) :

#![allow(unused)]
fn main() {
/// 芯片的状态被存储在一个 config 结构体中,它是在配置过程中由芯片生成,
/// 并且存储在芯片内部。
#[derive(Clone, Debug)]
struct FieldConfig {
    /// 对于这块芯片,我们将用到两个 advice 列来实现我们的指令集。
    /// 它们也是我们与电路的其他部分通信所需要用到列。
    advice: [Column<Advice>; 2],
    instance: Column<Instance>, //公开输入(instance)列

    // 我们需要一个 selector 来激活乘法门,从而在用不到`NumericInstructions::mul`指令的
    //cells 上不设置任何约束。这非常重要,尤其在构建更大型的电路的情况下,列会被多条指令集用到
    s_mul: Selector,

    /// 用来加载常数的 fixed 列
    constant: Column<Fixed>,
}

下面我们来构建约束 :

  • the most critical functions configure and enable_equality are used to check the equality of the incoming parameters(传入的参数).
  • 如下图 : 在 create_gate 函数中 :
    • 乘数 分别在同一行的 advice 列 ;
    • 乘积 同在 列, 的下一行 :
  • 可以看到在代码中, 都是使用 相对位置来描述的 !
#![allow(unused)]
fn main() {
// | a0  | a1  | s_mul | 
// |-----|-----|-------|
// | lhs | rhs | s_mul |
// | out |     |       |
let lhs = meta.query_advice(advice[0], Rotation::cur());
let rhs = meta.query_advice(advice[1], Rotation::cur());
let out = meta.query_advice(advice[0], Rotation::next()); // Attention !!
}

最后函数返回多项式约束:

  • s_mul 不为 0,则激活校验乘法约束 :
    • s_mul * (lhs * rhs - out) == 0,则 lhs * rhs = out 约束成立;
    • s_mul * (lhs * rhs - out) != 0lhs * rhs = out 约束不成立;程序报错
  • s_mul 为 0,则不会激活检查乘法约束,any subsequent values are fine
#![allow(unused)]
fn main() {
impl<F: FieldExt> FieldChip<F> {
    fn construct(config: <Self as Chip<F>>::Config) -> Self {
        Self {
            config,
            _marker: PhantomData,
        }
    }

    fn configure(
        meta: &mut ConstraintSystem<F>,
        advice: [Column<Advice>; 2],
        instance: Column<Instance>,
        constant: Column<Fixed>,
    ) -> <Self as Chip<F>>::Config {
        meta.enable_equality(instance.into());
        meta.enable_constant(constant);
        for column in &advice {
            meta.enable_equality((*column).into());
        }
        let s_mul = meta.selector();

        // 定义我们的乘法门
        meta.create_gate("mul", |meta| {
            // To implement multiplication, we need 3 advice `cells` 
            // and 1 selector.  We arrange them like so:
            //
            // | a0  | a1  | s_mul |
            // |-----|-----|-------|
            // | lhs | rhs | s_mul |
            // | out |     |       |
            //
            // 门可以用任一相对偏移,但每一个不同的偏移都会对证明增加开销。
            // 最常见的偏移值是 0 (当前行), 1(下一行), -1(上一行)。
            // 针对这三种情况,有特定的构造函数来构造`Rotation` 结构。
            let lhs = meta.query_advice(advice[0], Rotation::cur());
            let rhs = meta.query_advice(advice[1], Rotation::cur());
            let out = meta.query_advice(advice[0], Rotation::next());
            let s_mul = meta.query_selector(s_mul);

            // 最终,我们将约束门的多项式表达式返回。
            // 对于我们的乘法门,我们仅需要一个多项式约束。
            //
            // `create_gate` 函数返回的多项式表达式,在证明系统中一定等于0。
            // 我们的表达式有以下性质:
            // - 当 s_mul = 0 时,lhs, rhs, out 可以是任意值。
            // - 当 s_mul != 0 时,lhs, rhs, out 将满足 lhs * rhs = out 这条约束。 
            vec![s_mul * (lhs * rhs - out)]
        });

        FieldConfig {
            advice,
            instance,
            s_mul,
            constant,
        }
    }
}
}
}

Implement chip Traits

The instructions interface we defined earlier needs to be implemented, and defining the implementation of Number is to encapsulate(封装) finite field elements.

the most critical function mul , take out 2 cells from config , check whether the input and are equal, and then return :

#![allow(unused)]
fn main() {
region.constrain_equal(a.cell, lhs)?;
region.constrain_equal(b.cell, rhs)?;
// ...
let value = a.value.and_then(|a| b.value.map(|b| a * b));
}

It should be noted that, in addition to row and column, the position of the cell can also be determined by the relative position offset (除了行和列之外,单元的位置还可以通过相对位置偏移来确定).

Generally, there are 3 types of offsets, 0 representing the current position, 1 representing the next position, and -1 representing the previous position.

fn main() {
/// 用于表示数的变量
#[derive(Clone)]
struct Number<F: FieldExt> {
    cell: Cell,
    value: Option<F>,
}

impl<F: FieldExt> NumericInstructions<F> for FieldChip<F> {
    type Num = Number<F>;

    fn load_private(
        &self,
        mut layouter: impl Layouter<F>,
        value: Option<F>,
    ) -> Result<Self::Num, Error> {
        let config = self.config();

        let mut num = None;
        layouter.assign_region(
            || "load private",
            |mut region| {
                let cell = region.assign_advice(
                    || "private input",
                    config.advice[0],
                    0,
                    || value.ok_or(Error::SynthesisError),
                )?;
                num = Some(Number { cell, value });
                Ok(())
            },
        )?;
        Ok(num.unwrap())
    }

    fn load_constant(
        &self,
        mut layouter: impl Layouter<F>,
        constant: F,
    ) -> Result<Self::Num, Error> {
        let config = self.config();

        let mut num = None;
        layouter.assign_region(
            || "load constant",
            |mut region| {
                let cell = region.assign_advice_from_constant(
                    || "constant value",
                    config.advice[0],
                    0,
                    constant,
                )?;
                num = Some(Number {
                    cell,
                    value: Some(constant),
                });
                Ok(())
            },
        )?;
        Ok(num.unwrap())
    }

    fn mul(
        &self,
        mut layouter: impl Layouter<F>,
        a: Self::Num,
        b: Self::Num,
    ) -> Result<Self::Num, Error> {
        let config = self.config();

        let mut out = None;
        layouter.assign_region(
            || "mul",
            |mut region: Region<'_, F>| {
                // 在这个 region 中,我们只想用一个乘法门,所以我们在 region 偏移 0 处,
                //  激活它;这意味着它将对 0 偏移 和 1偏移处的两个 cells 进行约束。
                config.s_mul.enable(&mut region, 0)?;

                // 给我们的输入有可能在电路的任一位置,但在当前 region 中,我们仅可以用
                // 相对偏移。所以,我们在 region 内分配新的 cells 并限定他们的值与输入 cells 的值相等。
                let lhs = region.assign_advice(
                    || "lhs",
                    config.advice[0], // 第 0 列 (advice 列)
                    0,                // 第 0 行
                    || a.value.ok_or(Error::SynthesisError), // 放 a 的值进去
                )?;
                let rhs = region.assign_advice(
                    || "rhs",
                    config.advice[1], // 第 1 列, 
                    0,                // 第 0 行
                    || b.value.ok_or(Error::SynthesisError),
                )?;
                region.constrain_equal(a.cell, lhs)?;
                region.constrain_equal(b.cell, rhs)?;

                // 现在我们可以把乘积放到输出的位置了。
                let value = a.value.and_then(|a| b.value.map(|b| a * b));
                let cell = region.assign_advice(
                    || "lhs * rhs",
                    config.advice[0], // 第 0 列,  (advice 列)
                    1,                // 第 "1" 行 !!
                    || value.ok_or(Error::SynthesisError), // 放 value 的值进去
                )?;

                // 最后,我们返回一个用来表示输出的变量,它会被用在电路的另一个部分。
                out = Some(Number { cell, value });
                Ok(())
            },
        )?;

        Ok(out.unwrap())
    }

    fn expose_public(
        &self,
        mut layouter: impl Layouter<F>,
        num: Self::Num,
        row: usize,
    ) -> Result<(), Error> {
        let config = self.config();

        layouter.constrain_instance(num.cell, config.instance, row)
    }
}}

Build the Circuit

既然我们已经有了所需要的指令,以及一块实现了这些指令的芯片,我们终于可以构造示例电路啦

The circuit trait is the entrance to the circuit development. We need to define our own circuit structure and access the witness input.

struct MyCircuit :

  • 在这个结构体中,我们保存隐私输入变量。我们使用 Option<F> 类型是因为,在生成密钥阶段,他们不需要有任何的值。在证明阶段中,如果它们任一为 None 的话,将得到一个错误。

The interfaces defined before are all used here. configure creates a storage column for advice/instance/constant. synthesize uses a custom chip to get the input witness and constant, and finally, calculate the result and return the public input.

In fact, it can satisfy most scenarios by simply implementing the circuit trait for general circuit development. Some common functions of the chip have already been implemented in Halo 2.

#![allow(unused)]
fn main() {
struct MyCircuit<F: FieldExt> {
    constant: F,
    a: Option<F>,
    b: Option<F>,
}

impl<F: FieldExt> Circuit<F> for MyCircuit<F> {
    // 因为我们在任一地方值用了一个芯片,所以我们可以重用它的配置。
    type Config = FieldConfig;
    type FloorPlanner = SimpleFloorPlanner;

    fn without_witnesses(&self) -> Self {
        Self::default()
    }

    fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
        // 我们创建两个 advice 列,作为 FieldChip 的输入。
        let advice = [meta.advice_column(), meta.advice_column()];

        // 我们还需要一个 instance 列来存储公开输入。
        let instance = meta.instance_column();

        // 创建一个 fixed 列来加载常数
        let constant = meta.fixed_column();

        FieldChip::configure(meta, advice, instance, constant)
    }
    // Prove a2⋅b2 = c
    fn synthesize(
        &self,
        config: Self::Config,
        mut layouter: impl Layouter<F>,
    ) -> Result<(), Error> {
        let field_chip = FieldChip::<F>::construct(config);

        // 将我们的隐私值加载到电路中。
        let a = field_chip.load_private(layouter.namespace(|| "load a"), self.a)?;
        let b = field_chip.load_private(layouter.namespace(|| "load b"), self.b)?;

        // 将常数因子加载到电路中
        let constant =
            field_chip.load_constant(layouter.namespace(|| "load constant"), self.constant)?;

        // 我们仅有乘法可用,因此我们按以下方法实现电路:
        //     asq  = a*a
        //     bsq  = b*b
        //     absq = asq*bsq
        //     c    = constant*asq*bsq
        //
        // 但是,按下面的方法实现,更加高效: 
        //     ab   = a*b
        //     absq = ab^2
        //     c    = constant*absq
        let ab = field_chip.mul(layouter.namespace(|| "a * b"), a, b)?;
        let absq = field_chip.mul(layouter.namespace(|| "ab * ab"), ab.clone(), ab)?;
        let c = field_chip.mul(layouter.namespace(|| "constant * absq"), constant, absq)?;

        // 将结果作为电路的公开输入进行公开
        field_chip.expose_public(layouter.namespace(|| "expose c"), c, 0)
    }
} }
}

Testing the circuit

The MockProver and CircuitLayout that we mentioned in the chapter about tools can come in handy (派上用场).

可以用 halo2::dev::MockProver 来测试一个电路是否正常工作。构造电路的一组 Private/Public input ,这组输入可直接用来计算合法证明,但我们把这组输入传入到 MockProver::run 函数中之后,就能得到一个可用于检验电路中每一条约束是否满足的对象。而且电路验证不过,这个对象还能输出那条不满足的约束

如下代码 , MockProver::run 中只知道 Public input c , 他并不知道 Private 是什么, 但仍可以进行验证

fn main() {
    // 我们电路的行数不能超过 2^k. 因为我们的示例电路很小,我们选择一个较小的值
    let k = 4;

    // 准备好电路的隐私输入和公开输入
    let constant = Fp::from(7);
    let a = Fp::from(2);
    let b = Fp::from(3);
    let c = constant * a.square() * b.square();  // 算出来

    // 用隐私输入来实例化电路
    let circuit = MyCircuit {
        constant,
        a: Value::known(a),
        b: Value::known(b),
    };

    // 将公开输入进行排列。乘法的结果被我们放置在 instance 列的第0行,
    // 所以我们把它放在公开输入的对应位置。
    let mut public_inputs = vec![c];

    // 给定正确的公开输入,我们的电路能验证通过
    let prover = MockProver::run(k, &circuit, vec![public_inputs.clone()]).unwrap();
    assert_eq!(prover.verify(), Ok(()));

    // 如果我们尝试用其他的公开输入(此处是 +1),证明将失败!
    public_inputs[0] += Fp::one();
    let prover = MockProver::run(k, &circuit, vec![public_inputs]).unwrap();
    assert!(prover.verify().is_err());
}

Code Ref / Full example

You can find the source code for this example here.

cargo run --example simple-example

Question : 这份代码我没明白的是, 你可以随意修改 的值, 也不影响 Test 通过, 感觉少了一些

Goal : Given , we will prove .

    1, 1, 2, 3, 5, 8, 13, ...

    | elem_1 | elem_2 | sum | q_fib
    --------------------------------
    |    1   |    1   |  2  |   1
    |    1   |    2   |  3  |   1
    |    2   |    3   |  5  |   1
    |        |        |     |   0

    q_fib * (elem_1 + elem_2 - elem_3) = 0

![[Fibonacci(halo2).excalidraw]]

struct config

#![allow(unused)]
fn main() {
struct Config {
    elem_1: Column<Advice>,
    elem_2: Column<Advice>,
    elem_3: Column<Advice>,
    q_fib: Selector,
}
}

impl Config { ...

fn configure

#![allow(unused)]
fn main() {
fn configure<F: Field>(cs: &mut ConstraintSystem<F>) -> Self {
	// before using Permutation Argument, you need to enable_quality...
	let elem_1 = cs.advice_column();
	cs.enable_equality(elem_1);
	let elem_2 = cs.advice_column();
	cs.enable_equality(elem_2);
	let elem_3 = cs.advice_column();
	cs.enable_equality(elem_3);
	let q_fib = cs.selector();

	cs.create_gate("fibonacci", | virtual_cells | {
		let q_fib = virtual_cells.query_selector(q_fib);
		// if Rotation::next`  means  `ωx`
		// 这 3 列都在同一行, 所以都是 Rotation::cur()
		let elem_1 = virtual_cells.query_advice(elem_1, Rotation::cur());
		let elem_2 = virtual_cells.query_advice(elem_2, Rotation::cur());
		let elem_3 = virtual_cells.query_advice(elem_3, Rotation::cur());
        // q_fib * (elem_1 + elem_2 - elem_3) = 0
		vec![ q_fib * (elem_1 + elem_2 - elem_3), ]
	});
	Self { elem_1, elem_2, elem_3, q_fib }
}
}

Q: virtual_cells 是什么意思 ?

A by GPT : 在Halo2的上下文中,virtual_cells 这个名字可能不是最自明的,但其核心思想是,当我们定义一个新的门或约束时,我们需要一种方式来引用和查询电路的各个部分。这就是 virtual_cells 所做的事情。

configure 函数中定义门时,你通常会为其提供一个闭包,该闭包描述了你的约束如何工作。为了描述这些约束,你需要查询电路的某些值。这些值可能在建议的列、查找列或固定列中。

这就是 virtual_cells 的作用:它提供了一个方法来查询这些值,不管它们是在哪个列中。这就是为什么你会看到代码类似于:

#![allow(unused)]
fn main() {
let q_fib = virtual_cells.query_selector(q_fib);
let elem_1 = virtual_cells.query_advice(elem_1, Rotation::cur()); // ....
}

使用 virtual_cells 允许你以一个统一的方式来查询这些值,并将它们用于你的约束描述。这使得电路的定义更加清晰,尤其是当你需要处理多种不同类型的列时。

总之,virtual_cells 为电路的约束提供了查询接口,让你可以在定义门或约束时访问和操作它们。

fn init

init 负责初始化 Fibonacci 序列前 个数 , 返回第 个数

  • offset 定义了该区域中的行的位置 (第一行)
  • q_fib Selector 选择器: 某约束在某一行上是否需要成立
#![allow(unused)]
fn main() {
fn init<F: Field>(
  &self,
  mut layouter: impl Layouter<F>,
  elem_1: Value<F>,
  elem_2: Value<F>,
) -> Result<(
  AssignedCell<F, F>, // elem_2
  AssignedCell<F, F>  // elem_3
), Error> {
  layouter.assign_region(|| "init Fibonacci", |mut region| {
    let offset = 0;  // offset 定义了该区域中的行的位置 (第一行)            
    self.q_fib.enable(&mut region, offset)?;  // Enable q_fib

    // Assign elem_1
    region.assign_advice(|| "elem_1", self.elem_1, offset, || elem_1)?;

    // Assign elem_2
    let elem_2 = region.assign_advice(|| "elem_2", self.elem_2, offset, || elem_2)?;

    let e3_val = elem_1 + elem_2.value_field().evaluate();
    // Assign elem_3
    let elem_3 = region.assign_advice(|| "elem_3", self.elem_3, offset, || e3_val)?;
    Ok ((elem_2, elem_3))
  })
}
}

fn assign

copy_advice vs assign_advice i.e. 复制 vs. 重新赋值:

  • 当我们说“复制”,我们实际上是说我们要确保一个 Region-Cell 的值与另一个 Region-Cell 中的值是相同的。与其为每个地方重新计算/分配(assign)一个值,不如简单地“复制”该值到新位置,以确保它们是一样的。
  • Permutations and Copy Constraints: Halo2使用一种称为"permutation argument"的技术来确保两个或多个单元格中的值是相同的。copy_advice 实际上是在背后使用这个技术,通过引入一个额外的约束来确保值的一致性。
#![allow(unused)]
fn main() {
fn assign<F: Field>(
	&self,
	mut layouter: impl Layouter<F>,
	elem_2: AssignedCell<F, F>,
	elem_3: AssignedCell<F, F>,
) -> Result<(
	AssignedCell<F, F>, // elem_2
	AssignedCell<F, F>  // elem_3
), Error> {
	layouter.assign_region(|| "steady-state Fibonacci", |mut region| {
		let offset = 0;

		// Enable q_fib
		self.q_fib.enable(&mut region, offset)?;

		// Copy elem_1 (which is the previous elem_2)
		let elem_1 = elem_2.copy_advice(|| "copy elem_2 into current elem_1", &mut region, self.elem_1, offset)?;

		// Copy elem_2 (which is the previous elem_3)
		let elem_2 = elem_3.copy_advice(|| "copy elem_3 into current elem_2", &mut region, self.elem_2, offset)?;

		let e3_val = elem_1.value_field().evaluate() + elem_2.value_field().evaluate();
		// Assign elem_3
		let elem_3 = region.assign_advice(|| "elem_3", self.elem_3, offset, || e3_val)?;

		Ok((
			elem_2,
			elem_3
		))
	})
}
}

Q: 为什么 elem_2.copy_advice 没有使用 assign_advice ?

A: 如下 : 正在将前一个 Region 中的 elem_2 复制到当前 Region 中的 elem_1。这意味着你希望当前 Region 的 elem_1 和前一个 Region 的 elem_2 持有相同的值

#![allow(unused)]
fn main() {
let elem_1 = elem_2.copy_advice(|| "copy elem_2 into current elem_1", &mut region, self.elem_1, offset)?;
}

为什么要这样做?因为 Fibonacci 序列中的下一个数字是前 2 个数字的和。你正在滑动序列,所以前一个数字(前一个区域的 elem_2)现在成为当前区域的 elem_1

Test

#![allow(unused)]
fn main() {
    #[test]
    fn test_fib() {

        let circuit = MyCircuit {
            elem_1: Value::known(Fp::one()), // 1
            elem_2: Value::known(Fp::from(2)), // 1 ??
        };

        let prover = MockProver::run(3, &circuit, vec![]).unwrap();
        prover.assert_satisfied();
    }
}

laiyingtong 的这份代码我没明白的是, 你可以随意修改 elem_1 / elem_2 的值, 也不影响 Test 通过, 感觉少了一些 Private input ?

cargo test

Reference :

  • https://github.com/therealyingtong/halo2-hope/blob/main/src/fibonacci.rs

Q: 在 Example 2 的 assign 函数中,

example 1

struct components

ACell
  • ACell 是一个 tuple struct ,
#![allow(unused)]
fn main() {
use std::marker::PhantomData;

use halo2_proofs::{arithmetic::FieldExt, circuit::*, plonk::*, poly::Rotation};

#[derive(Debug, Clone)]
struct ACell<F: FieldExt>(AssignedCell<F, F>);
}

Why ACell ?

  1. 封装和抽象:通过使用 ACell,我们为用户提供了一个简化和更直观的接口,使他们可以更容易地与已分配的单元格进行交互,而不必每次都直接处理 AssignedCell
  2. 灵活性:将来,如果我们想在 ACell 中添加更多的功能或属性,我们可以这样做而不影响现有的代码。
  3. 故 : ACell 主要是一个辅助结构体,用于简化与电路中单元格的交互。

元素访问 :

#![allow(unused)]
fn main() {
// 因为 `ACell` 是对 `AssignedCell` 的简单包装,
// 所以可以直接使用 `.0` 语法来访问其内部的 `AssignedCell` :  `prev_b.0`
let c_val = prev_b.0.value().copied() + prev_c.0.value();
}

.map 访问 :

  • 具体来说,assign_advice 返回的是 Result<AssignedCell<F, F>, Error>.map(ACell) 会将其转换为 Result<ACell<F>, Error>
  • 元组结构体本身可以作为函数来调用, 相当于调用一个带有一个参数的构造函数。
#![allow(unused)]
fn main() {
// when call .map() , 我们提供一个函数,将其应用于 Result 内的 Ok 的值(if so)
// 本例中传递的函数是 ACell 的构造函数,所以我们是将 AssignedCell 转换成 ACell
// 对于 tuple struct, 如 `let black = Color(0, 0, 0);`
// therefore  `AssignedCell<F, F>` 本身是一个函数
let a_cell = region
    .assign_advice(|| "a", self.config.advice[0], 0, || a)
    .map(ACell)?;
}
FiboConfig / FiboChip
#![allow(unused)]
fn main() {
struct FiboConfig {
    pub advice: [Column<Advice>; 3],
    pub selector: Selector,
    pub instance: Column<Instance>,
}

struct FiboChip<F: FieldExt> {
    config: FiboConfig, // ↑ 👆🏻
    _marker: PhantomData<F>,
}
}

impl FiboChip { ...

fn configure
  • meta: 是对约束系统的可变引用,允许我们在其中配置列和约束。
  • Selector : 用于激活或禁用某些特定约束
  • meta.query_selector : Query a selector at the current position.
  • Query an advice column at a relative position : Query an advice column at a relative position
#![allow(unused)]
fn main() {
impl<F: FieldExt> FiboChip<F> {
    pub fn construct(config: FiboConfig) -> Self {
        Self {
            config,
            _marker: PhantomData,
        }
    }

    pub fn configure(
        meta: &mut ConstraintSystem<F>,
        advice: [Column<Advice>; 3],
        instance: Column<Instance>,
    ) -> FiboConfig {
        let col_a = advice[0]; // 对每个 advice 列进行命名
        let col_b = advice[1];
        let col_c = advice[2];
        let selector = meta.selector();

        meta.enable_equality(col_a);
        meta.enable_equality(col_b);
        meta.enable_equality(col_c);
        meta.enable_equality(instance);

        meta.create_gate("add", |meta| {
            //
            // col_a | col_b | col_c | selector
            //   a      b        c       s
            //
            // Query a selector at the current position.
            let s = meta.query_selector(selector);
            let a = meta.query_advice(col_a, Rotation::cur());
            let b = meta.query_advice(col_b, Rotation::cur());
            let c = meta.query_advice(col_c, Rotation::cur());
            vec![s * (a + b - c)]
        });

        FiboConfig {
            advice: [col_a, col_b, col_c],
            selector,
            instance,
        }
    }
}
fn assign_first_row
  • 本函数的作用: 为 Fibonacci list 的第一行的前 2 个元素分配值 1 , 返回前 3 个元素 a_cell, b_cell, c_cell
  • layouter.assign_region
    • region.assign_advice(|| "a", self.config.advice[0], 0, || a)
    • 函数源码: [[halo2 Source Code#assign_advice]]
#![allow(unused)]
fn main() {
#[allow(clippy::type_complexity)]
pub fn assign_first_row(
	&self,
	mut layouter: impl Layouter<F>,
	a: Value<F>,
	b: Value<F>,
) -> Result<(ACell<F>, ACell<F>, ACell<F>), Error> {
	layouter.assign_region(
		|| "first row",
		|mut region| {
			self.config.selector.enable(&mut region, 0)?;

			let a_cell = region
				.assign_advice(|| "a", self.config.advice[0], 0, || a)
				.map(ACell)?;

			let b_cell = region
				.assign_advice(|| "b", self.config.advice[1], 0, || b)
				.map(ACell)?;

			let c_cell = region
				.assign_advice(|| "c", self.config.advice[2], 0, || a + b)
				.map(ACell)?;

			Ok((a_cell, b_cell, c_cell))
		},
	) }
}
fn assign_row
  • layouter.assign_region
    • copy_advice()prev_bprev_c 的值复制到当前行的 前 2 列(a/b column)。这意味着前一个b值被复制到新行的第一列(标记为a),前一个c值被复制到新行的第二列(标记为b
    • 计算新的斐波那契数c_val,它是prev_bprev_c的和。
    • 使用assign_advice分配c_val到新行的第三列,并返回此值的 ACell
#![allow(unused)]
fn main() {
pub fn assign_row(
  &self, // 当前`FiboChip`实例的引用
  mut layouter: impl Layouter<F>,
  prev_b: &ACell<F>,
  prev_c: &ACell<F>,  // Fibonacci 数列中的前 2 个数字
) -> Result<ACell<F>, Error> {
  layouter.assign_region(
    || "next row",
    |mut region| {
      self.config.selector.enable(&mut region, 0)?;

      prev_b
        .0
        .copy_advice(|| "a", &mut region, self.config.advice[0], 0)?;
      prev_c
        .0
        .copy_advice(|| "b", &mut region, self.config.advice[1], 0)?;

      let c_val = prev_b.0.value().copied() + prev_c.0.value();

      let c_cell = region
        .assign_advice(|| "c", self.config.advice[2], 0, || c_val)
        .map(ACell)?;

      Ok(c_cell)
    },
  )
}
}

fn expose_public

  • expose_public : 将指定的 ACell 公开为 Public Input.
#![allow(unused)]
fn main() {
pub fn expose_public(
	&self,
	mut layouter: impl Layouter<F>,
	cell: &ACell<F>,
	row: usize,
) -> Result<(), Error> {
	layouter.constrain_instance(cell.0.cell(), self.config.instance, row)
}

// 
chip.expose_public(layouter.namespace(|| "private a"), &prev_a, 0)?;
chip.expose_public(layouter.namespace(|| "private b"), &prev_b, 1)?;
}

MyCircuit

  1. let chip = FiboChip::construct(config); : 传入 config 创建一个新的 FiboChip 实例
  2. chip.assign_first_row(layouter.namespace(|| "first row"), self.a, self.b)?; : 初始化斐波那契数列: 调用 assign_first_row 函数以在第一行中设置斐波那契数列的前两个值 self.a 和 self.b。返回的结果是三个值:prev_a, prev_b 和 prev_c。其中,prev_c 是前两个数的和
  3. chip.expose_public(layouter.namespace(|| "private a"), &prev_a, 0)?; : 公开前两个数: 将前两个数 expose 为 public, 这意味着这些值可以被 访问和验证
  4. 计算后续的斐波那契数: for 循环中,assign_row 函数被调用以计算后续的斐波那契数。每次迭代都会生成新的斐波那契数并为下一次迭代更新 prev_bprev_c
  5. chip.expose_public(layouter.namespace(|| "out"), &prev_c, 2)?; : 公开最终的斐波那契数: 将循环结束后的最后一个斐波那契数值设为 Public
#![allow(unused)]
fn main() {
#[derive(Default)]
struct MyCircuit<F> {
    pub a: Value<F>,
    pub b: Value<F>,
}

impl<F: FieldExt> Circuit<F> for MyCircuit<F> {
    type Config = FiboConfig;
    type FloorPlanner = SimpleFloorPlanner;

    fn without_witnesses(&self) -> Self {
        Self::default()
    }

    fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
        let col_a = meta.advice_column();
        let col_b = meta.advice_column();
        let col_c = meta.advice_column();
        let instance = meta.instance_column();
        FiboChip::configure(meta, [col_a, col_b, col_c], instance)
    }

    fn synthesize(
        &self,
        config: Self::Config,
        mut layouter: impl Layouter<F>,
    ) -> Result<(), Error> {
        let chip = FiboChip::construct(config);

        let (prev_a, mut prev_b, mut prev_c) =
            chip.assign_first_row(layouter.namespace(|| "first row"), self.a, self.b)?;

        chip.expose_public(layouter.namespace(|| "private a"), &prev_a, 0)?;
        chip.expose_public(layouter.namespace(|| "private b"), &prev_b, 1)?;

        for _i in 3..10 {
            let c_cell = chip.assign_row(layouter.namespace(|| "next row"), &prev_b, &prev_c)?;
            prev_b = prev_c;
            prev_c = c_cell;
        }

        chip.expose_public(layouter.namespace(|| "out"), &prev_c, 2)?;

        Ok(())
    }
}
}

Test

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::MyCircuit;
    use halo2_proofs::{circuit::Value, dev::MockProver, pasta::Fp};

    #[test]
    fn test_example1() {
        let k = 4;

        let a = Fp::from(1); // F[0]
        let b = Fp::from(1); // F[1]
        let out = Fp::from(55); // F[9]

        let circuit = MyCircuit {
            a: Value::known(a),
            b: Value::known(b),
        };

        let mut public_input = vec![a, b, out];

        let prover = MockProver::run(k, &circuit, vec![public_input.clone()]).unwrap();
        prover.assert_satisfied();

        public_input[2] += Fp::one();
        let _prover = MockProver::run(k, &circuit, vec![public_input]).unwrap();
        // uncomment the following line and the assert will fail
        // _prover.assert_satisfied();
    }
}
cargo test fibonacci::example1  `or`
cargo test -- --nocapture fibonacci::example1

Example 2

the problem we noticed like inside example-1 is that there are basically

  • too many duplicate cells , so every time you need copy two cells from previous row to next row , not efficient
  • better solution : Use rotation to access to the multiple rows.

在本例子中, 代码将更加紧凑和模块化, 以下是一些主要的区别 :

  1. Advice Columns:
    • ex 1 :
      • 用了 3 个 advice columns:col_a, col_bcol_c,这 3 个 columns 的第一行用来存储 Fibonacci 序列的连续的 3 个数
      • configure() 在 3 个 advice columns 中为每一个需启用 enable_equality,并为每一个都建立了门约束。
    • ex 2:
      • 仅使用了一个 advice column,并依赖 rotation(轮转, 即 )来访问连续的数, 减少了各种复制
#![allow(unused)]
fn main() {
// example-1
pub fn configure(
	.., advice: [Column<Advice>; 3], .. 
	{
    let col_i = advice[ii];	
    meta.enable_equality(col_i); // 很多列需要声明, 需要 enable...

    meta.create_gate("add", |meta| {
        let s = meta.query_selector(selector);
        let a = meta.query_advice(col_a, Rotation::cur()); //📢
        let b = meta.query_advice(col_b, Rotation::cur()); //📢
        let c = meta.query_advice(col_c, Rotation::cur()); //📢

}

----------------------------------------

// example-2
pub fn configure(  
	.., advice: Column<Advice>, .. 
	{
    meta.enable_equality(advice); //所有 advice 列只在此 enable once.

    meta.create_gate("add", |meta| {
        let s = meta.query_selector(selector);
        let a = meta.query_advice(advice, Rotation::cur()); //💡
        let b = meta.query_advice(advice, Rotation::next());//💡
        let c = meta.query_advice(advice, Rotation(2));}    //💡
}
  • Rotation::cur() 当前行
  • Rotation::next() 下一行
  • Rotation(2) 再下一行

The Polynomial Identity :

  1. 数据赋值:
    • ex 1: 初始的 Fibonacci 数 ab 被赋值到两个不同的 advice columns,而他们的和则被赋值到第三个 column。
    • ex 2: 所有的 Fibonacci 数都在同一个 advice column,但在不同的行 (thanks to rotation)
#![allow(unused)]
fn main() {
////  Example 2  ////
pub fn assign(..., nrows){
  layouter.assign_region("entire fibonacci table",
    |mut region| {
    // 为前两行启用 selector,这意味着我们将从 instance 列(可能是公共输入)
    // 中复制 Fibonacci 序列的前 2 个数字
    self.config.selector.enable(&mut region, 0)?;
    self.config.selector.enable(&mut region, 1)?;

    // assign_advice_from_instance 方法,将 instance 列的前两个值
	//   (即 Fibonacci 序列的前两个数字)赋给 advice 列中的前两个单元格
	//   后面在 MockProver 中, 我们会传入 instance 作为 Public input
	let mut a_cell = region.assign_advice_from_instance(
		|| "1",
		self.config.instance,
		0,
		self.config.advice,
		0,  // row
	)?;
	let mut b_cell = region.assign_advice_from_instance(
		|| "1",
		self.config.instance,
		1,  // 这里写 0 也不报错, 写 1/2/3 就会报错了..
		self.config.advice,
		1,  // row
    )?;
    
	// 赋值好了前 2 行(递归基) ,其余的行就累加过去就好了
	for row in 2..nrows {
		if row < nrows - 2 {
			self.config.selector.enable(&mut region, row)?;
		}

		let c_cell = region.assign_advice(
			|| "advice",
			self.config.advice,
			row,
			|| a_cell.value().copied() + b_cell.value(),
		)?;

		a_cell = b_cell;
		b_cell = c_cell;
	}

	Ok(b_cell)	
}
}
  1. 生成的 Fibonacci 数:
    • Version 1: 使用方法 assign_row 从前两个数生成下一个数。
    • Version 2: 使用一个循环在整个 Fibonacci 表格中为所有的数赋值。
#![allow(unused)]
fn main() {
////  Example 2  ////
pub fn assign(..., nrows){
	// 赋值好了前 2 行(递归基) ,其余的行就累加过去就好了
	for row in 2..nrows {
		if row < nrows - 2 {
			self.config.selector.enable(&mut region, row)?;
		}

		let c_cell = region.assign_advice(
			|| "advice",
			self.config.advice,
			row,
			|| a_cell.value().copied() + b_cell.value(),
		)?;

		a_cell = b_cell;
		b_cell = c_cell;
	}

	Ok(b_cell)
}
}

如下 instance , 里面是 Public input

#![allow(unused)]
fn main() {
#[test]
fn test_example2() {
	let k = 4;

	let a = Fp::from(1); // F[0]
	let b = Fp::from(1); // F[1]
	let out = Fp::from(55); // F[9]

	let circuit = MyCircuit(PhantomData);

	let mut public_input = vec![a, b, out];

	let prover = MockProver::run(k, &circuit, vec![public_input.clone()]).unwrap();
	prover.assert_satisfied();

	public_input[2] += Fp::one();
	let _prover = MockProver::run(k, &circuit, vec![public_input]).unwrap();
	// uncomment the following line and the assert will fail
	// _prover.assert_satisfied();
}
}

print

  • the white column is the instance column,
  • the pink one is the advice and
  • the purple one is the selector.
  • the green part shows the cells that have been assigned
    • light green : selector not used.
cargo test --all-features -- --nocapture print

change k from 13 to 4, the line will be more small so now you are not calim about the main function .

  • the MockProver will tell you constrains that ,
  • the png will tell you a    constraint you have ignored !

Row & Column in Region

Compared example-1 with example-2 :

#![allow(unused)]
fn main() {
meta.create_gate("add", |meta| {
	// col_a | col_b | col_c | selector
	//   a      b        c       s
	let s = meta.query_selector(selector);
	let a = meta.query_advice(col_a, Rotation::cur());
	let b = meta.query_advice(col_b, Rotation::cur());
	let c = meta.query_advice(col_c, Rotation::cur());
	vec![s * (a + b - c)]

---------------------------------------------

meta.create_gate("add", |meta| {
	// advice | selector
	//   a    |   s
	//   b    |
	//   c    |
	let s = meta.query_selector(selector);
	let a = meta.query_advice(advice, Rotation::cur());
	let b = meta.query_advice(advice, Rotation::next());
	let c = meta.query_advice(advice, Rotation(2));
	vec![s * (a + b - c)] 
}

We see :

  • col_a / col_b / col_c represent different
  • Rotation::cur() / Rotation::next() / Rotation::prev() / Rotation(2) represent different
cargo test -- --nocapture fibonacci::example2

Reference :

We want Prove that : f(a, b, c) = if a == b {c} else {a - b}

证明某人知道三个数字 a、b和 c,使得当 a == b 时,输出为 c,否则输出为 a - b,而无需揭示a、b和c的实际值。

how to describe it ? Firstly, let's dive into the Iszero Chip

Iszero Chip

structs

#![allow(unused)]
fn main() {
#[derive(Clone, Debug)]
pub struct IsZeroConfig<F> {
    pub value_inv: Column<Advice>,
    pub is_zero_expr: Expression<F>,
}

impl<F: FieldExt> IsZeroConfig<F> {
    pub fn expr(&self) -> Expression<F> {
        self.is_zero_expr.clone()
    }
}

pub struct IsZeroChip<F: FieldExt> {
    config: IsZeroConfig<F>,
}
}

impl IsZeroChip { ..

configure
#![allow(unused)]
fn main() {
impl<F: FieldExt> IsZeroChip<F> {
    pub fn construct(config: IsZeroConfig<F>) -> Self {
        IsZeroChip { config }
    }

    pub fn configure(
        meta: &mut ConstraintSystem<F>,
        q_enable: impl FnOnce(&mut VirtualCells<'_, F>) -> Expression<F>,
        value: impl FnOnce(&mut VirtualCells<'_, F>) -> Expression<F>,
        value_inv: Column<Advice>,
    ) -> IsZeroConfig<F> {
        let mut is_zero_expr = Expression::Constant(F::zero());

        meta.create_gate("is_zero", |meta| {
            //
            // valid | val |  val_inv |  1 - val * val_inv | val * (1 - val * val_inv)
            // ------+-----+----------+--------------------+-------------------
            //  yes  |  x  |    1/x   |        0           |   0
            //  no   |  x  |    0     |        1           |   x
            //  yes  |  0  |    0     |        1           |   0
            //  yes  |  0  |    y     |        1           |   0

            //
            let value = value(meta);
            let q_enable = q_enable(meta);
            let value_inv = meta.query_advice(value_inv, Rotation::cur());

            is_zero_expr = Expression::Constant(F::one()) - value.clone() * value_inv;
            vec![q_enable * value * is_zero_expr.clone()]  // gate's constraints
        });

        IsZeroConfig {
            value_inv,
            is_zero_expr,
        }
    }
}

configure defines the logic for the "is-zero" gate. It uses the following table to guide the logic:

valid | val |  val_inv |  1 - val * val_inv | val * (1 - val * val_inv)
------+-----+----------+--------------------+-------------------
 yes  |  x  |    1/x   |        0           |   0
 no   |  x  |    0     |        1           |   x
 yes  |  0  |    0     |        1           |   0
 yes  |  0  |    y     |        1           |   0

1 / 3 / 4 行涉及到的约束不需要通过 q_enable 即可完成, 但是考虑第二行所涉及到的情况 :

  • 如果 是个 malicious Prover, 他提供了 val == xval_inv == 0 , 此时仅靠 is_zero_expr 是无法分辨的 (这个 case 里 assign 函数会直接分配 self.config.value_inv i.e. 即认为这个值是
  • 但是添加了 vec![q_enable * value * is_zero_expr.clone()] 约束就不一样了 , 约束强制要求 val * is_zero_expr i.e. val * ( 1 - val * val_inv) 必须为 0 , 从而解决了这种 malicious situation.
  • 如果 malicious 提供了这种 Witness, 将不会通过约束校验, 也就不会生成该 proof
  • 只有 提供了符合约束的 Witness, val_inv 才会被赋值给 val_inv column

The gate ensures that for valid rows:

  • If the , its inverse is computed such that their multiplication (val * val_inv) 's results in 1.
  • If the , its inverse can be any value, but the result of their multiplication should be 0.

The gate equation is q_enable * value * (1 - value * value_inv), which should be satisfied for the valid conditions.

  • assign(): This method is used to assign the inverse of a value (if it exists) or zero to the specified advice column in the circuit.
#![allow(unused)]
fn main() {
is_zero_expr = Expression::Constant(F::one()) - value.clone() * value_inv;
}
  • i.e. 1 - val * val_inv , like the table above :
    • if val != 0 : is_zero_expr = 0
    • if val == 0 : is_zero_expr = 1

vec![q_enable * value * is_zero_expr.clone()] is the gate's constraint. it should be

assign
#![allow(unused)]
fn main() {
pub fn assign(
	&self,
	region: &mut Region<'_, F>,
	offset: usize,
	value: Value<F>,
) -> Result<(), Error> {
	// value.invert()  OR  F::zero()
	let value_inv = value.map(|value| value.invert().unwrap_or(F::zero()));
	region.assign_advice(|| "value inv", self.config.value_inv, offset, || value_inv)?;
	Ok(())
}
}

IsZero 的验证过程中,将要验证的值(或输入值)分配到电路区域中,以便在电路中进行计算和约束的验证 :

  1. 如果要验证的值为零,assign 方法将为逆元分配一个特定的值(例如 F::zero()
  2. 如果要验证的值不为零,value_inv columns 将被分配为 value.invert().unwrap_or(F::zero()) i.e. value.invert()

这些 IsZero 的 check 将被赋值到 value_inv column 并在其上得到体现

Example 3

welcome back, now we have the gadget IsZero , so we can constrain malicious 's input

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
struct FunctionConfig<F: FieldExt> {
    selector: Selector,
    a: Column<Advice>,
    b: Column<Advice>,
    c: Column<Advice>,
    a_equals_b: IsZeroConfig<F>,
    output: Column<Advice>,
}
#[derive(Debug, Clone)]
struct FunctionChip<F: FieldExt> {
    config: FunctionConfig<F>,
}
}

configure

Recap : f(a, b, c) = if a == b {c} else {a - b}

  1. column : 除了常规的 a/b/c advice column, 还申请了 is_zero_advice_column
  2. IsZeroChip : use crate::is_zero::{IsZeroChip, IsZeroConfig}; 使用了上面定义的 IsZero chip 来校验 这个事情 (因为 a/b 都是 提供的, 一个 malicious 有动机去提供 a=3 , b=4 然后 return c , 必须通过生成 proof 前的约束来限制 的行为)
  3. IsZeroChip::configure 返回 IsZeroConfig<F>
#![allow(unused)]
fn main() {
impl<F: FieldExt> FunctionChip<F> {
    pub fn construct(config: FunctionConfig<F>) -> Self { Self { config } }

    pub fn configure(meta: &mut ConstraintSystem<F>) -> FunctionConfig<F> {
        let selector = meta.selector();
        let a = meta.advice_column();
        let b = meta.advice_column();
        let c = meta.advice_column();
        let output = meta.advice_column();

        let is_zero_advice_column = meta.advice_column();
        
        let a_equals_b = IsZeroChip::configure(
            meta,
            |meta| meta.query_selector(selector),
            |meta| meta.query_advice(a, Rotation::cur()) - meta.query_advice(b, Rotation::cur()),
            is_zero_advice_column,
        );

        meta.create_gate("f(a, b, c) = if a == b {c} else {a - b}", |meta| {
            let s = meta.query_selector(selector);
            let a = meta.query_advice(a, Rotation::cur());
            let b = meta.query_advice(b, Rotation::cur());
            let c = meta.query_advice(c, Rotation::cur());
            let output = meta.query_advice(output, Rotation::cur());
            vec![
                s.clone() * (a_equals_b.expr() * (output.clone() - c)),
                s * (Expression::Constant(F::one()) - a_equals_b.expr()) * (output - (a - b)),
            ]
        });

        FunctionConfig {
            selector,
            a,
            b,
            c,
            a_equals_b,
            output,
        }
    }
}

assign

  1. IsZeroChip::construct : 创建一个IsZeroChip实例
  2. layouter.assign_region( :
#![allow(unused)]
fn main() {
pub fn assign(
  &self,
  mut layouter: impl Layouter<F>,
  a: F,  b: F,  c: F,
) -> Result<AssignedCell<F, F>, Error> {
  let is_zero_chip = IsZeroChip::construct(self.config.a_equals_b.clone());

  layouter.assign_region(
    || "f(a, b, c) = if a == b {c} else {a - b}",
    |mut region| {
      self.config.selector.enable(&mut region, 0)?;
      region.assign_advice(|| "a", self.config.a, 0, || Value::known(a))?;
      region.assign_advice(|| "b", self.config.b, 0, || Value::known(b))?;
      region.assign_advice(|| "c", self.config.c, 0, || Value::known(c))?;

      // 正式使用 IsZeroChip 子电路来检查 a - b 是否为零
      is_zero_chip.assign(&mut region, 0, Value::known(a - b))?;

      // Rust expr to calculate val.
      let output = if a == b { c } else { a - b };
      // assign to cell.
      region.assign_advice(|| "output", self.config.output, 0, || Value::known(output))
    },
  ) }
}

test

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;
    use halo2_proofs::{dev::MockProver, pasta::Fp};

    #[test]
    fn test_example3() {
        let circuit = FunctionCircuit {
            a: Fp::from(10),
            b: Fp::from(12),
            c: Fp::from(15),
        };

        let prover = MockProver::run(4, &circuit, vec![]).unwrap();
        prover.assert_satisfied();
    }
}
}

assign_advice

halo2_proofs/src/circuit.rs

  • assign_advice() 是为了在指定的 advice column 和偏移(行)中分配一个值,并返回该已分配值的 wrapping,i.e. AssignedCell

参数 :

  • &'v mut self: 代表函数需要一个 self 的可变引用,并且这个引用的生命周期至少为 'v
  • annotation: A: 是一个返回注释字符串的函数。可用于为调试或错误消息提供上下文
  • column: Column<Advice>: 指定应该在哪一列分配该值。
  • offset: usize: 指定应该在哪一行分配该值。
  • mut to: V: 是一个函数,它返回将要被分配的值。这是一个闭包或函数对象

where 子句指定了该函数的泛型参数必须满足的约束:

  • V 是一个可变函数对象,返回一个 Value<VR>
  • Assigned<F> 必须实现 From trait,从一个对 VR 的引用转换。
  • A 是一个函数对象,返回一个 AR
  • AR 必须能转换为字符串,这是为了 annotation 能够提供描述性的字符串。

Process :

  1. 函数首先声明了一个未知的值:let mut value = Value::unknown();
  2. 然后,它使用 self.region.assign_advice 方法来真正分配值,传递注释、列、偏移和一个获取值的闭包。此闭包先调用 to() 获取值,然后转换该值为字段,并更新之前声明的未知值。
  3. 最后,一个新的 AssignedCell 被构造并返回,其中包含已分配的值和对应的单元格。
  4. assign_advice 函数为一个给定的建议列和行分配一个值,并返回一个表示已分配值的 AssignedCell。这个函数设计得非常泛化,允许客户端提供任意的值提供函数和注释函数。
#![allow(unused)]
fn main() {
pub fn assign_advice<'v, V, VR, A, AR>(
	&'v mut self,
	annotation: A,
	column: Column<Advice>,
	offset: usize,
	mut to: V,
) -> Result<AssignedCell<VR, F>, Error>
where
	V: FnMut() -> Value<VR> + 'v,
	for<'vr> Assigned<F>: From<&'vr VR>,
	A: Fn() -> AR,
	AR: Into<String>,
{
	let mut value = Value::unknown();
	let cell =
		self.region
			.assign_advice(&|| annotation().into(), column, offset, &mut || {
				let v = to();
				let value_f = v.to_field();
				value = v;
				value_f
			})?;

	Ok(AssignedCell {
		value,
		cell,
		_marker: PhantomData,
	})
}
}

assign_advice_from_instance

  • 将绝对位置“row”处的 instance col cell 的值分配给该区域内“offset”处的列“advice”。
#![allow(unused)]
fn main() {
/// Assign the value of the instance column's cell at absolute location
/// `row` to the column `advice` at `offset` within this region.
///
/// Returns the advice cell, and its value if known.
pub fn assign_advice_from_instance<A, AR>(
	&mut self,
	annotation: A,
	instance: Column<Instance>,
	row: usize,
	advice: Column<Advice>,
	offset: usize,
) -> Result<AssignedCell<F, F>, Error>
where
	A: Fn() -> AR,
	AR: Into<String>,
{
	let (cell, value) = self.region.assign_advice_from_instance(
		&|| annotation().into(),
		instance,
		row,
		advice,
		offset,
	)?;

	Ok(AssignedCell {
		value,
		cell,
		_marker: PhantomData,
	})
}
}

without_witnesses

  • called only during keygen
  • the circuit without witnesses

synthesize

with takes the output of the 'configure' and the floorplanner to make the actual circuit

  • called at keygen and proving time
  • has copy constraints
  • this is the witness generation code, so you don't need to do much halo2-specific stuff except call assign once per region

第一代 zkp 算法: groth16 , groth16 算法的每一个新的业务, 每次迭代都需要重新做 Setup, 模式太重, 需要强信任

后面的 universal setup 优化: 只需要做一次 Setup, 所有的业务都可以使用. 最初代的 universal setup 算法是 Sonic, poly-com 用了 KZG, 但是性能不够

19 年 ZCash 团队设计出了 Halo 算法, 使用了 Inner Product argument 替换 KZG, 把实现 universal Setup 的条件更进一步削弱了, 改成了不需要 Setup . Halo2 不光想支持 SNARK 算法, 他更想支持递归特性, 而因为 Sonic 的验证是线性的, 所以其提出了一套 嵌套均摊 (nested amortization) 技术

History :

  • Sonic : 支持 universal Setup 的 SNARK (性能不够)
  • Halo: Sonic + Inner Product argument + 嵌套均摊 (nested amortization) 技术
  • PLONK : 对 Sonic 的改进
  • Plookup : PLONK + lookup table
  • UltraPLONK : Plookup + custom gate (需要 Setup)
  • Halo2 : UltraPLONK + 嵌套均摊 (nested amortization) 技术 (无需 Setup)
    • 通过 nested amortization 将递归证明的 overhead 降低

总结下 :

  • Groth16 : 第一代 Pairing-based zk-SNARKs
  • PLONK : 第二代 Pairing-based zk-SNARKs (universal )
  • Halo2 : 第三代 zkp 技术, YYDS !
    • 不需要 Setup
    • 不需要配对曲线, 只需要一个支持离散对数的曲线, 需要的安全性假设较弱
    • 支持递归

Concepts

The arithmetization used by Halo 2 comes from PLONK, or more precisely its extension UltraPLONK that supports custom gates and lookup arguments. We'll call it PLONKish.

Inner Product Argument

星想法对 [Inner Product Argument] 的介绍

  • No Setup

Custom Gate

Custom Gate 可以引用同一列的多行 Cell, 类似于 d 、 d_next 的作用

  • 代价 : 证明中需要多增加一个 即多增加一个 Fr Custom Gate 的表达式是一个多元高次多项式, 即, 最高次数可以 >2
  • Standard PLONK : Degree = 2 每一个 Custom Gate 都有一个对应的 selector column 与其对应

Custom Gate 只能够引用相对偏移的 cells, 偏移值的类型是

![[Pasted image 20230822142938.png]] 一个 Custom Gate 可以涉及到若干个 advice/Instance/fixed/selector 它可以引用相对位置(上 1 行, 上 2 行, 下 1 行, 下 2 行)

如上图有 2 个 gates, 对于 , 其 设置为 1 , 则需要满足 对于下面那个, 都是 1 , 则 Custom Gate 0 / 1 都需要满足

Halo2 Arithmetization

  • Halo 里用 R1CS 来表述 Circuit,模式如:
  • Halo2 里用 Plonk Arithmetization 来表示 Circuit,模式如:

其中, 为 selector polynomial, 不同的取值,代表不同的 Gate,如下表所示:

Halo2 编程

![[Pasted image 20230822150314.png]]

电路进阶 (sha256 优化实现)

  • Groth16 + R1CS 实现 SHA256 :
    • 约需要 2.5W 行 R1CS 约束 ...
  • Halo2 + Custom Gate + lookup table :
    • 2099 行 (影响的是多项式的次数 : Groth16 中多项式的次数至少是 25000, 这里只需要 2099 次数)
    • 11 advice columns
    • 3 fixed constant columns
    • 20 Custom Gates.
    • 20 Selector columns (控制 Custom Gates.)
    • Spread Table 使用了 行 ( 使用的是 u16, 所以是 行)
      • 为什么 远大于 2099 ?

Halo2电路构建源代码导读

  • PLONK/Groth16 约束系统由一条条相对独立的约束组成(约束只能约束该行上的变量),并且一条约束指定了支持的计算(乘法或者加法组合)
  • Halo2 的约束系统中的约束并不限定在一行上的变量,并且采用“custom gate”,任意指定约束需要的计算

Cell : Cell指定在一个Region中的某行某列 , 其定义在 src/circuit.rs :

#![allow(unused)]
fn main() {
pub struct Cell {  
    region_index: RegionIndex,  
    row_offset: usize,  
    column: Column<Any>,  
}
}

Region :

  • 给 Cell 进行配置的时候, 是需要按照一个 Region 来的, Region 可以认为是小的功能模块, Region 在论文里是没有的, 是为了方便开发实现加进去的逻辑.
  • 在一个 Region 里面的逻辑可以 copy, 在写电路的时候看到的是一个一个的 Region, 在生成另外一个 Region 的时候, 可以把相同的逻辑做平移, 因为都是使用的 相对坐标
    • (为了更好的电路模块化,电路构建往往是相对于一个 Region, 同一个电路模块,可以在不同的 Region 中“复制”)
  • 开发人员写代码关注的事 Region 里的实现 —— 在 Region 里干什么事, 然后 Region 会自己找合适的地方放置这些 Chip

Chip/cfig/instruction/Layouter

  • Chip(芯片) : 电路由一个个 Chip 逻辑堆砌而成。每个 Chip 的创建从 “Config”开始。
  • Config : 所谓的 Config,就是申请Chip需要的Column以及配置Fixed列的逻辑含义。这些配置可能是 Custom Gate,可能是 lookup。
  • Instructions : 每一个 Chip 都有指令(Instruction)。通过指令的调用,将 Chip 的部分或者全部功能加入电路中
  • Layouter
    • Layouter 做的事就是根据需求, 找到能放下 Chip 的空地, 把他放置上去, 类似在一个矩形上画东西: 把 Chip 放到画布上, 就像华容道, 组出来的矩阵可大可小, 有好有坏...
    • 将 Chip 添加到一个电路上需要布局。Layouter 就是用来实现布局。Layouter 接口定义在src/circuit.rs中:

Layouter 本身存在层级关系,所以 Layouter Interface 定义了get_root / push_namespace / pop_namespace / namespace 等相关的函数。核心逻辑在其他三个函数:

  • assign_region - 获取一个 region,在这个 region 上可以进行各种“assignment”(赋值),定义在 RegionLayouter 接口中
  • assign_table - 获取一个 table,并设置 table,接口定义在 TableLayouter 接口中。
  • constrain_instance - 限制一个 Cell 和 Instance 列中的某个 Cell 一致
#![allow(unused)]
fn main() {
pub trait Layouter<F: Field> {  
    type Root: Layouter<F>;  
      
    fn assign_region<A, AR, N, NR>(&mut self, name: N, assignment: A) -> Result<AR, Error>  
    where  
        A: FnMut(Region<'_, F>) -> Result<AR, Error>,  
        N: Fn() -> NR,  
        NR: Into<String>;  
  
    fn assign_table<A, N, NR>(&mut self, name: N, assignment: A) -> Result<(), Error>  
    where  
        A: FnMut(Table<'_, F>) -> Result<(), Error>,  
        N: Fn() -> NR,  
        NR: Into<String>;  
  
    fn constrain_instance(  
        &mut self,  
        cell: Cell,  
        column: Column<Instance>,  
        row: usize,  
    ) -> Result<(), Error>;  
  
    fn get_root(&mut self) -> &mut Self::Root;  
    fn push_namespace<NR, N>(&mut self, name_fn: N)  
    where  
        NR: Into<String>,  
        N: FnOnce() -> NR;  
    fn pop_namespace(&mut self, gadget_name: Option<String>);  
    fn namespace<NR, N>(&mut self, name_fn: N) -> NamespacedLayouter<'_, F, Self::Root>  
    where  
        NR: Into<String>,  
        N: FnOnce() -> NR,  
    {  
        self.get_root().push_namespace(name_fn);  
  
        NamespacedLayouter(self.get_root(), PhantomData)  
    }  
}
}

Assignment

  • 前面提到, 布局的过程中用的 Region, 但是其被放置的画布是一个全局的矩阵, 在 synthesize 完成后, 是要对所有的 Cells 进行赋值的
  • 电路“赋值”的接口 就是 Assignment ,定义在 src/plonk/circuit.rs
  • Assignment 处理的东西比较 Tricky:
    • 它既要知道 Region 信息(Region 用的是一些 relative 相对信息)
    • 又要把 Region 信息翻译 Translate 成全局信息, 因为最后结果是全局的, 中间的 Assign 过程是曲折的, 是通过 Region 进行转换的 ...

电路内部框架

为了方便 Devs 开发电路,Halo2 的内部抽象了布局的接口 , 目前有 2 套 Layouter 的实现:

  1. SimpleFloorPlanner
  2. V1/V1Plan 为了理解 Halo2 的内部逻辑,这里只详细讲解 SimpleFloorPlanner :

![[Pasted image 20230824151908.png]]

如上图, SimpleFloorPlanner 处在最核心高层, 是一个更高阶的管理器 , Layouter 是水平布局器, 基本上只负责一层的构建

整个框架由四个接口组成:FloorPlannerLayouterRegionLayout/TableLayout 以及Assignment

简单的说,一个 FloorPlanner 拥有一个 Layouter,一个 Layouter 可以分配多个 RegionLayout 或者TableLayout。电路对 Cell 的 assignment 通过 Assignment 实现

先从三者的整体调用关系讲起:

  • SimpleFloorPlanner 是对 FloorPlanner 接口的实现,定义在 src/circuit/floor_planner/single_pass.rs 中:
#![allow(unused)]
fn main() {
pub struct SimpleFloorPlanner;

// SimpleFloorPlanner实现了FloorPlanner trait 的synthesize函数。
// 容易看出,该函数创建出SingleChipLayouter对象,并直接调用相应的synthesize函数开始电路的synthesize
impl FloorPlanner for SimpleFloorPlanner {
    fn synthesize<F: Field, CS: Assignment<F>, C: Circuit<F>>(
        cs: &mut CS,
        circuit: &C,
        config: C::Config,
        constants: Vec<Column<Fixed>>,
    ) -> Result<(), Error> {
        let layouter = SingleChipLayouter::new(cs, constants)?;
        circuit.synthesize(config, layouter)
    }
}
}

SingleChipLayouter 定义在 src/circuit/floor_planner/single_pass.rs 中,包括了电路的所有的信息。

#![allow(unused)]
fn main() {
pub struct SingleChipLayouter<'a, F: Field, CS: Assignment<F> + 'a> {
    cs: &'a mut CS,
    constants: Vec<Column<Fixed>>,
    /// Stores the starting row for each region.
    regions: Vec<RegionStart>,
    /// Stores the first empty row for each column.
    columns: HashMap<RegionColumn, usize>,
    /// Stores the table fixed columns.
    table_columns: Vec<TableColumn>,
    _marker: PhantomData<F>,
}
}

SingleChipLayouter 的 Region 管理比较简单,某行整体属于某个 Region 。

  • regions 记录每个 Region 的行的开始偏移(starting row)。
  • cs 是 Assignment 接口的实现,存储所有电路的赋值信息。
  • columns 记录当前操作 RegionColumn 对应的空的 row 的偏移。table_columns 记录 Table 需要的 Fixed 的 Column。
  • 简单的说,SingleChipLayouter 记录了电路(包括布局)需要的所有信息。

SingleChipLayouter's assign_region 函数实现一个 Region 的 synthesize 过程。简单的说,SingleChipLayouter 的 assign_region 函数的主要逻辑就是创建一个 region ,并将 region 内的布局转化为全局布局。SingleChipLayouter 的 assign_region 逻辑可以分成两部分:

  1. 通过 RegionShape 获取 Region 的“形状”。所谓的“形状”,包括主要是采用的 Column 的信息
  2. 根据上一步获取的 Column 信息,找出和其他 Region 不冲突的起始问题。
#![allow(unused)]
fn main() {
// 
fn assign_region<A, AR, N, NR>(&mut self, name: N, mut assignment: A) -> Result<AR, Error>
where
    A: FnMut(Region<'_, F>) -> Result<AR, Error>,
    N: Fn() -> NR,
    NR: Into<String>,
{
    let region_index = self.regions.len(); // 获取当前Region的编号

    // Get shape of the region. 这个 Region 有几行几列?
    // 调用 RegionShape 的 Region 接口,收集该 Region 的电路涉及到的 Column 信息
    let mut shape = RegionShape::new(region_index.into());
    {
        let region: &mut dyn RegionLayouter<F> = &mut shape;
        assignment(region.into())?;
    }

    // Lay out this region. We implement the simplest approach here: position the
    // region starting at the earliest row for which none of the columns are in use.
    let mut region_start = 0; //根据收集到的Column信息,获取Region开始的行号
    for column in &shape.columns {
        region_start = cmp::max(region_start, self.columns.get(column).cloned().unwrap_or(0));
    }
    self.regions.push(region_start.into());

    // Update column usage information. //在Region中记录下所有使用的Column的信息
    for column in shape.columns {
        self.columns.insert(column, region_start + shape.row_count);
    }

    // Assign region cells.
    self.cs.enter_region(name); //创建Region
    let mut region = SingleChipLayouterRegion::new(self, region_index.into());
    let result = {
        let region: &mut dyn RegionLayouter<F> = &mut region;
        assignment(region.into()) //采用SingleChipLayouterRegion对电路赋值
    }?;
    let constants_to_assign = region.constants;
    self.cs.exit_region(); //退出Region

    // Assign constants. For the simple floor planner, we assign constants in order in
    // the first `constants` column.
    if self.constants.is_empty() {//如果制定了constants,需要增加置换限制
        if !constants_to_assign.is_empty() {
            return Err(Error::NotEnoughColumnsForConstants);
        }
    } else {
        let constants_column = self.constants[0];
        let next_constant_row = self
            .columns
            .entry(Column::<Any>::from(constants_column).into())
            .or_default();
        for (constant, advice) in constants_to_assign {
            self.cs.assign_fixed(
                || format!("Constant({:?})", constant.evaluate()),
                constants_column,
                *next_constant_row,
                || Ok(constant),
            )?;
            self.cs.copy(
                constants_column.into(),
                *next_constant_row,
                advice.column,
                *self.regions[*advice.region_index] + advice.row_offset,
            )?;
            *next_constant_row += 1;
        }
    }

    Ok(result)
}
}

类似 0XPARC 里提到的, 即使第二行可以放置一个 Region, 但是 SimpleFloorPlanner 管不了那么多, 我就是往后一行放 ... (这个电路是一直往下的, 不会向左找找, 向右找找, 是比较初级的)

SingleChipLayouter's assign_table : 当前的电路中增加查找表逻辑:

#![allow(unused)]
fn main() {
fn assign_table<A, N, NR>(&mut self, name: N, mut assignment: A) -> Result<(), Error>  
where  
    A: FnMut(Table<'_, F>) -> Result<(), Error>,  
    N: Fn() -> NR,  
    NR: Into<String>,  
{  
    // Maintenance hazard: there is near-duplicate code in `v1::AssignmentPass::assign_table`.  
    // Assign table cells.  
    self.cs.enter_region(name); //创建一个Region  
    let mut table = SimpleTableLayouter::new(self.cs, &self.table_columns);  
    {  
        let table: &mut dyn TableLayouter<F> = &mut table;  
        assignment(table.into()) //查找表赋值  
    }?;  
    let default_and_assigned = table.default_and_assigned;  
    self.cs.exit_region(); //退出当前Region  
  
    // Check that all table columns have the same length `first_unused`,  
    // and all cells up to that length are assigned.  
    let first_unused = { //获取出所有查找表相关的Column对应的最大使用行数  
        match default_and_assigned  
            .values()  
            .map(|(_, assigned)| {  
                if assigned.iter().all(|b| *b) {  
                    Some(assigned.len())  
                } else {  
                    None  
                }  
            })  
            .reduce(|acc, item| match (acc, item) {  
                (Some(a), Some(b)) if a == b => Some(a),  
                _ => None,  
            }) {  
            Some(Some(len)) => len,  
            _ => return Err(Error::SynthesisError), // TODO better error  
        }  
    };  
  
    // Record these columns so that we can prevent them from being used again.  
    for column in default_and_assigned.keys() {  
        self.table_columns.push(*column);  
    }  
    //根据default_and_assigned信息,采用default值扩展所有的column  
    for (col, (default_val, _)) in default_and_assigned {  
        // default_val must be Some because we must have assigned  
        // at least one cell in each column, and in that case we checked  
        // that all cells up to first_unused were assigned.  
        self.cs  
            .fill_from_row(col.inner(), first_unused, default_val.unwrap())?;  
    }  
  
    Ok(())  
}
}

default_and_assigned 记录了在一个 Fixed Column 上的 default 值以及某个 cell 是否已经设置

SingleChipLayouterRegion 实现了 Region 接口。如果在 Region 中需要给一个 Advice 列中 Cell 赋值,可以采用 assign_advice 函数:

fn assign_advice<'v>(  
    &'v mut self,  
    annotation: &'v (dyn Fn() -> String + 'v),  
    column: Column<Advice>,  
    offset: usize,  
    to: &'v mut (dyn FnMut() -> Result<Assigned<F>, Error> + 'v),  
) -> Result<Cell, Error> {  
    self.layouter.cs.assign_advice( //调用Assignment接口设置相应的Cell信息,特别注意的是在设置的时候需要Cell的全局偏移  
        annotation,  
        column,  
        *self.layouter.regions[*self.region_index] + offset,  
        to,  
    )?;  
  
    Ok(Cell {  
        region_index: self.region_index,  
        row_offset: offset,  
        column: column.into(),  
    })  
}

cs.assign_fixed 函数,对 fixed Column 进行赋值。可以参考 MockProver 的实现(src/dev.rs)

  • 这个 cs 不是 constrain System , 只是 Assignment 的接口 : MockProver
#![allow(unused)]
fn main() {
fn assign_fixed<V, VR, A, AR>(
    &mut self,
    _: A,
    column: Column<Fixed>,
    row: usize,
    to: V,
) -> Result<(), Error>
where
    V: FnOnce() -> Result<VR, Error>,
    VR: Into<Assigned<F>>,
    A: FnOnce() -> AR,
    AR: Into<String>,
{
    ... //在一些检查后,设置Fixed列中的某个Cell(column,row指定)
    *self
        .fixed
        .get_mut(column.index())
        .and_then(|v| v.get_mut(row))
        .ok_or(Error::BoundsFailure)? = CellValue::Assigned(to()?.into().evaluate());

    Ok(())
}
}

cs.copy 函数,增加置换信息 :

#![allow(unused)]
fn main() {
fn copy(
    &mut self,
    left_column: Column<Any>,
    left_row: usize,
    right_column: Column<Any>,
    right_row: usize,
) -> Result<(), crate::plonk::Error> {
    if !self.usable_rows.contains(&left_row) || !self.usable_rows.contains(&right_row) {
        return Err(Error::BoundsFailure);
    }

    self.permutation //增加Permutation信息
        .copy(left_column, left_row, right_column, right_row)
}
}

接着我们再详细看看 RegionLayouter 和 TableLayouter 。RegionLayouter 定义在src/circuit/layouter.rs:

#![allow(unused)]
fn main() {
pub trait RegionLayouter<F: Field>: fmt::Debug {
    //enable选择子
    fn enable_selector<'v>(
        &'v mut self,
        annotation: &'v (dyn Fn() -> String + 'v),
        selector: &Selector,
        offset: usize,
    ) -> Result<(), Error>;

    //advice或者fixed赋值
    fn assign_advice<'v>(
        &'v mut self,
        annotation: &'v (dyn Fn() -> String + 'v),
        column: Column<Advice>,
        offset: usize,
        to: &'v mut (dyn FnMut() -> Result<Assigned<F>, Error> + 'v),
    ) -> Result<Cell, Error>;

    fn assign_advice_from_constant<'v>(
        &'v mut self,
        annotation: &'v (dyn Fn() -> String + 'v),
        column: Column<Advice>,
        offset: usize,
        constant: Assigned<F>,
    ) -> Result<Cell, Error>;

    fn assign_advice_from_instance<'v>(
        &mut self,
        annotation: &'v (dyn Fn() -> String + 'v),
        instance: Column<Instance>,
        row: usize,
        advice: Column<Advice>,
        offset: usize,
    ) -> Result<(Cell, Option<F>), Error>;

    fn assign_fixed<'v>(
        &'v mut self,
        annotation: &'v (dyn Fn() -> String + 'v),
        column: Column<Fixed>,
        offset: usize,
        to: &'v mut (dyn FnMut() -> Result<Assigned<F>, Error> + 'v),
    ) -> Result<Cell, Error>;

    //cell相等约束
    fn constrain_constant(&mut self, cell: Cell, constant: Assigned<F>) -> Result<(), Error>;

    fn constrain_equal(&mut self, left: Cell, right: Cell) -> Result<(), Error>;
}
}

RegionLayouter 的接口很容易理解,包括设置选择子,给cell赋值,约束cell相等。

TableLayouter 的接口定义如下:

#![allow(unused)]
fn main() {
pub trait TableLayouter<F: Field>: fmt::Debug {
    fn assign_cell<'v>(
        &'v mut self,
        annotation: &'v (dyn Fn() -> String + 'v),
        column: TableColumn,
        offset: usize,
        to: &'v mut (dyn FnMut() -> Result<Assigned<F>, Error> + 'v),
    ) -> Result<(), Error>;
}
}

TableLayouter 只有一个接口:assign_cell. assign_cell 是对表中的某个TableColumn的Cell进行赋值。

至此,大体的电路构造的逻辑的框架相对清楚:Halo2 中的 Chip 电路由一个个 Region 组成,在Halo2 的框架中,Region 通过 Layouter 进行分配。电路的所有的信息都存储在 Assignment 的接口中。MockProver 是一个可以参考的 Assignment 的实现

ConstraintSystem

#![allow(unused)]
fn main() {
pub struct ConstraintSystem<F: Field> {
    pub(crate) gates: Vec<Gate<F>>,
    // ...
}

gates 描述了 “Custom Gate” 的限制表达式:

#![allow(unused)]
fn main() {
#[derive(Clone, Debug)]  
pub(crate) struct Gate<F: Field> {  
    name: &'static str,  
    constraint_names: Vec<&'static str>,  
    polys: Vec<Expression<F>>,   // Attention !!
    /// We track queried selectors separately from other cells, so that we can use them to  
    /// trigger debug checks on gates.  
    queried_selectors: Vec<Selector>,  
    queried_cells: Vec<VirtualCell>,  
}
}

如上代码, 这个 polys 多项式表达的逻辑是你自己书写的, 而不是像 R1CS 中, 就那几个门都固化了, 必须按照这几个门的约束来写

Halo2的电路构建分为两部分:

  1. Configure (电路配置)
  2. Synthesize(电路(实例)综合)

a^2 +b^2 示例

1 Configure 过程

Configure 调用 ConstraintSystem 申请各种列以及 Gate 的信息。调用某个 Circuit 的 Configure 函数会顺序调用电路涉及到的 Chip 的 Configure 信息,这些信息都记录在 ConstraintSystem 中

查看实例中的 Chip 的 Configure 函数 :

#![allow(unused)]
fn main() {
impl<F: FieldExt> Circuit<F> for MyCircuit<F> {
    ...
    fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
        let advice = [meta.advice_column(), meta.advice_column()];
        let instance = meta.instance_column();
        let constant = meta.fixed_column();

        FieldChip::configure(meta, advice, instance, constant)
    }
    ...
}

impl<F: FieldExt> FieldChip<F> {
    fn construct(config: <Self as Chip<F>>::Config) -> Self {
        Self {
            config,
            _marker: PhantomData,
        }
    }

    fn configure(
        meta: &mut ConstraintSystem<F>,
        advice: [Column<Advice>; 2],
        instance: Column<Instance>,
        constant: Column<Fixed>,
    ) -> <Self as Chip<F>>::Config {
        meta.enable_equality(instance.into());
        meta.enable_constant(constant);
        for column in &advice {
            meta.enable_equality((*column).into());
        }
        let s_mul = meta.selector();

        meta.create_gate("mul", |meta| {
            // | a0  | a1  | s_mul |
            // |-----|-----|-------|
            // | lhs | rhs | s_mul |
            // | out |     |       |
            let lhs = meta.query_advice(advice[0], Rotation::cur());
            let rhs = meta.query_advice(advice[1], Rotation::cur());
            let out = meta.query_advice(advice[0], Rotation::next());
            let s_mul = meta.query_selector(s_mul);

            vec![s_mul * (lhs * rhs - out)]
        });

        FieldConfig {
            advice,
            instance,
            s_mul,
            constant,
        }
    }
}
}

示例电路申请了两个 advice 列,一个 instance 和 fixed 列, 同时电路构造了一个乘法 Gate:

#![allow(unused)]
fn main() {
vec![s_mul * (lhs * rhs - out)]
}

该乘法 Gate 就是相应的限制表达式。注意和其他零知识证明的约束系统不一样的是,一个约束可以采用多个行上的 Cell 。整个调用关系如下:

synthesize

Dream : synthesize 过程就不要看代码了, 看着一张图就够了 在 Configure 完电路后,可以调用 synthesize 综合某个电路实例。整个调用关系如下:

![[Pasted image 20230824174216.png]]

看图, 某个 Chip 调用 Layouter 分配 Region,并在 Region 中指定约束。可以查看 FieldChip 的 mul 函数:

#![allow(unused)]
fn main() {
impl<F: FieldExt> NumericInstructions<F> for FieldChip<F> {
fn mul(
	&self,
	mut layouter: impl Layouter<F>,
	a: Self::Num,
	b: Self::Num,
) -> Result<Self::Num, Error> {
	let config = self.config();

	let mut out = None;
	layouter.assign_region(
		|| "mul",
		|mut region: Region<'_, F>| {
		    // 获取到一个Region后,enable该row对应的乘法selector。
			config.s_mul.enable(&mut region, 0)?; 

			let lhs = region.assign_advice( //对两个advice进行赋值
				|| "lhs",
				config.advice[0],
				0,
				|| a.value.ok_or(Error::SynthesisError),
			)?;
			let rhs = region.assign_advice(
				|| "rhs",
				config.advice[1],
				0,
				|| b.value.ok_or(Error::SynthesisError),
			)?;
			//限制两个 advice 和之前的 load 的 Cell 一致
			region.constrain_equal(a.cell, lhs)?; 
			region.constrain_equal(b.cell, rhs)?;

			// Now we can assign the multiplication result into the output position.
			let value = a.value.and_then(|a| b.value.map(|b| a * b));
			let cell = region.assign_advice( //对乘法的输出进行赋值
				|| "lhs * rhs",
				config.advice[0],
				1,
				|| value.ok_or(Error::SynthesisError),
			)?;

			// Finally, we return a variable representing the output,
			// to be used in another part of the circuit.
			out = Some(Number { cell, value });
			Ok(())
		},
	)?;

	Ok(out.unwrap())
} // ...
}
}

Summary

理解 Halo2,可以从两部分着手:1/ 电路构建 2/ 证明系统。从开发者的角度看,电路构建是接口。如何通过 Halo2 创建电路,这些电路在Halo2的内部如何表示是理解电路构建的关键。Halo2中的Chip电路由一个个 Region 组成,在 Halo2 的框架中,Region 通过 Layouter 进行分配。电路的所有的信息都存储在 Assignment 的接口中。Halo2的电路构建分为两部分: 1/Configure (电路配置) 2/ Synthesize(电路综合)。简单的说,Configure就是进行电路本身的配置。Synthesize进行某个电路实例的综合。

深入分析SuperNova及其ROM实现

针对于为有限状态机上程序执行的正确性的问题,Nova是集大成者,作者Setty提出了一种基于Folding的递归证明系统。但是Nova要求在迭代中使用相同的业务电路,可以理解为仅支持单个指令。SuperNova则对其进行了拓展,在每步迭代中可以运行不同的指令(这一问题定义为NIVC, non-uniform IVC),因此可以把Nova看作是只支持一个指令的NIVC解决方案。之前采用全局电路的方法其开销与所有指令构成的电路规模有关,Supernova最大的创新则是其证明开销只与当前步执行的指令有关,并且产生的overhead是常数。

本文首先介绍NIVC问题的定义,以及Supernova的基本思路,最后针对于文中没有给出具体说明的电路选择器,详细介绍了ROM模型的实现思路。

NIVC定义

image NIVC是对IVC的泛化,在每步增量计算中,Prover可以证明满足一些列relation中的一种relation,所以他可以支持每步使用不同的电路。 首先定义NIVC要证明的电路形式,假设存在个多项式时间可计算的函数 (可以把他们看作是执行一些列不同指令的电路),他们满足:,其中为选择器,其根据当前witness 和公共输出选择其中第个函数,即输出

Prover则是要生成proof,其可以证明对于n步迭代中产生的一系列均满足。可将其形式化表述为, 其中P为Prover,pk为prover key,为proof。与IVC类似,NIVC要求Prover在任意步的证明开销与之前调用的指令无关,否则会导致电路规模无限增大;更进一步要求Prover的开销只和当前步运行的电路规模有关,否则就蜕化成了用一个包含所有函数电路构成的IVC。

Supernova证明系统

对于上述证明问题,Supernova采用了类似与Nova的folding scheme,总体来说其也是先构建一个Augumented函数,通过证明存在满足的witness,来证明业务电路F以及每次迭代更新proof的正确性。每次需要把relaxed R1CS实例(U)和r1cs实例(u)进行fold,而且folding scheme要求两种实例的结构是一样的,但是NIVC中函数有多个,不能简单地fold。因此,SuperNova中在第会输入一系列的,其中代表从0到步被正确执行,这样只需要验证所有的是否满足约束就可以验证所有函数从0到步被正确执行;此外还会输入一个实例u,用来证明第步也被正确执行。 image 对于Augumented函数,相对于Nova的不同点在于,在第步只折叠第个实例,为了确保执行的是,需要将也作为公共输入放入中来进行检验。

Supernova证明系统的核心构造为: image image

说明:实际上Relaxed R1CS(Az◦Bz = uCz + E)和R1CS(Az◦Bz=Cz)中的A,B,C是一致(这些值由业务电路+Folding相关的约束生成),只是具体的z不一样。

需要注意的是上述证明系统并没有明确约束第步具体选择哪个电路,因此如果需要确定性生成相应的选择器,还需增加选择器电路。 然而论文中没有给出选择器的具体实现,下面参考PSE一位成员给出的一种电路序列固定的Supernova实现,进一步给出具体实现细节。

ROM machine based Supernova

Rom( read-only memory)模型将所有的电路看作电路序列,该序列共有个电路,其中不同的电路共有个,并将所有的电路直接写死在Supernova的公共输入中,每迭代一步, 则放入中。在第步时,读取,并选取对应的电路。 比如共有2个不同的电路,ROM构成的电路序列为

那么在Supernova论文给出的证明系统之上,还需保证:

  1. 步fold的是第个电路;
  2. 在第步选择是第电路(注意这点在supernova中没有要求)

对于第1个问题,主要通过构造一个条件选择电路,具体电路如下:

    // select target when index match last_augmented_circuit_index, other left as empty

    let U: Result<Vec<AllocatedRelaxedR1CSInstance<G>>, SynthesisError> = U
      .iter()
      .enumerate()
      .map(|(i, U)| {
        let i_alloc = alloc_const(
          cs.namespace(|| format!("U_i i{:?} allocated", i)),
          scalar_as_base::<G>(G::Scalar::from(i as u64)),
        )?;

        let equal_bit = Boolean::from(alloc_num_equals(
          cs.namespace(|| format!("check U {:?} equal bit", i)),
          &i_alloc,
          last_augmented_circuit_index,
        )?);

        conditionally_select_alloc_relaxed_r1cs(
          cs.namespace(|| format!("select on index namespace {:?}", i)),
          U,
          &empty_U,
          &equal_bit,
        )
      })
      .collect();

对于第2个电路,核心思路是构造 来实现其约束, 其中 value[i]=rom[i],i=pci0,i=pc[i]

具体代码如下:

fn constraint_augmented_circuit_index<F: PrimeField, CS: ConstraintSystem<F>>(
    mut cs: CS,
    pc_counter: &AllocatedNum<F>,
    rom: &[AllocatedNum<F>],
    circuit_index: &AllocatedNum<F>,
  ) -> Result<(), SynthesisError> {

    // select target when index match or empty
    let zero = alloc_zero(cs.namespace(|| "zero"))?;
    let rom_values = rom
      .iter()
      .enumerate()
      .map(|(i, rom_value)| {
        let index_alloc = alloc_const(
          cs.namespace(|| format!("rom_values {} index ", i)),
          F::from(i as u64),
        )?;

        let equal_bit = Boolean::from(alloc_num_equals(
          cs.namespace(|| format!("rom_values {} equal bit", i)),
          &index_alloc,
          pc_counter,
        )?);

        conditionally_select(
          cs.namespace(|| format!("rom_values {} conditionally_select ", i)),
          rom_value,
          &zero,
          &equal_bit,
        )
      })

      .collect::<Result<Vec<AllocatedNum<F>>, SynthesisError>>()?;

    let sum_lc = rom_values
      .iter()
      .fold(LinearCombination::<F>::zero(), |acc_lc, row_value| {
        acc_lc + row_value.get_variable()
      });

    println!("self.circuit index ==============> : {:?}", circuit_index.get_value());
    cs.enforce(
      || "sum_lc == circuit_index",
      |lc| lc + circuit_index.get_variable() - &sum_lc,
      |lc| lc + CS::one(),
      |lc| lc,

    );
    Ok(())

  }

致谢

非常感谢 SECBIT Labs 的 @郭宇老师对SuperNova研究方向的指导。

参考文献

零知识证明由于其本身陡峭的入门学习曲线,往往被初学者称为moon math。为了平缓学习曲线,减轻入门压力,babysnark[1]应运而生,本文将作为babysnark原理部分的一个解读版,帮助你更好的理解snark背后的一些基本概念和直觉。在阅读本文之前,希望你已经读过# 从零开始学习 zk-SNARK系列的前4部分,对包括有限域、椭圆曲线等相关知识有一个基本的了解。

R1CS

比如我们有这样一段程序:

def qeval(x):  
	y = x**3  
	return x + y + 5

我们知道程序执行实际上是CPU中的乘法门和加法门组合运算得到的。那么上面的程序可以看成是类似是下面的这个图,有一些输入变量和中间运算过程,最后得到输出。

alt_text

为了更好的表示中间过程是如何执行的,我们需要将上述程序拆分写成如下形式,左侧是中间运算的输出结果,右侧可以看成中间运算的输入:
sym_1 = x * x  
y = sym_1 * x  
sym_2 = y + x  
~out = sym_2 + 5

为什么我们输入一定要写成两个变量而不能是三个或者多个变量呢?具体限制原因可以从限制运算[3]中找到答案。简单来说,多项式的算数性质有在某一个具体的点上,左操作数和右操作数相乘等于输出结果。而这个约束特点使得每一次输入只能是两个数的形式,如果一次有多个变量作为输入,可以分别将其拆分成两两组合。

有了这样的直觉之后我们可以来看一下R1CS(Rank 1 constraint system)的具体定义:

给定三个m行n列的矩阵 , 和一个 维向量 定义了一组m个方 程,每个方程的形式如下:

其中 , ·表示矩阵和向量的乘积, 表示 的第 个元素。 等价地,我们可以使用Hadamard积(逐元素相乘)来表示整个系统:

其中○表示Hadamard积。

其中A可以看作是左操作数的全局结果的矩阵表示,B可以看成是右操作数全部结果的矩阵表示。C是运算结果的全部结果的矩阵表示。接下来让我们一步一步将上述4个等式转变成矩阵的Hadamard积的形式。

假设我们将上述4个等式的输入输出变量按如下顺序排列:

'~one', 'x', '~out', 'sym_1', 'y', 'sym_2'

那么对于第一个等式

sym_1 = x * x

左操作数a,右操作数b和最后结果c可以分别表示成如下向量形式

a = [0, 1, 0, 0, 0, 0]  
b = [0, 1, 0, 0, 0, 0]  
c = [0, 0, 0, 1, 0, 0]

然后向量和上述6个变量相乘,就可以还原出第一个等式了。类似的,我们对等式2,3,4做同样的处理,最终可以得到矩阵A,B,C:

A  
[0, 1, 0, 0, 0, 0]  
[0, 0, 0, 1, 0, 0]  
[0, 1, 0, 0, 1, 0]  
[5, 0, 0, 0, 0, 1]

B  
[0, 1, 0, 0, 0, 0]  
[0, 1, 0, 0, 0, 0]  
[1, 0, 0, 0, 0, 0]  
[1, 0, 0, 0, 0, 0]

C  
[0, 0, 0, 1, 0, 0]  
[0, 0, 0, 0, 1, 0]  
[0, 0, 0, 0, 0, 1]  
[0, 0, 1, 0, 0, 0]

通过上述操作,我们就将一段程序转换成了R1CS的形式。

多项式插值

在实际的零知识证明系统中,不管具体零知识证明算法是哪种,总要有一个validator发出一个随机数作为challenge,然后prover接受这个随机数作为系统输入,然后返回一个输出结果。validator拿到输出结果看是否和挑战的随机数满足某种对应关系,如果满足就认为prover确实掌握了某种知识。为了实现validator可以找任意随机数,所以我们就有必要R1CS的约束关系转换成多项式的形式。

比如对于之前的矩阵A而言,如果竖着按列看,其实其对应的就是之前文中所说的6个变量

'~one', 'x', '~out', 'sym_1', 'y', 'sym_2'

比如说,对于one变量而言,其在上述4个等式(即4种约束关系)中所组成的向量为

~one: [0, 0, 0, 5]

如果将其在笛卡尔坐标系中表示,假设我们选取x为1,2,3,4,那么该one所组成的多项式应该经过(1,0), (2,0), (3,0), (4,5)这4个点。在笛卡尔坐标系中,我们对于做操作数和有操作数以及结果的所有x坐标只要满足一致关系,他们所组成的多项式都满足R1CS约束关系。基于上述特点,我们可以对6个变量选定一致的x坐标然后使用插值的方式得到多项式的形式。下面是我们选定x坐标是1,2,3,4得到的矩阵A的多项式表示形式:

A polynomials  
[-5.0, 9.166, -5.0, 0.833]  
[8.0, -11.333, 5.0, -0.666]  
[0.0, 0.0, 0.0, 0.0]  
[-6.0, 9.5, -4.0, 0.5]  
[4.0, -7.0, 3.5, -0.5]  
[-1.0, 1.833, -1.0, 0.166]

即one可以表示为:

其他变量的R1CS转换也同理。

QAP

这种转换成的多项式新形式称之为QAP(Quadratic Arithmetic Program)我们来看一下QAP的具体定义。

定义(QAP): 一个在域 上的二次算术程序 包含三种 多项式:

  • 其中 ,以及一个目标多项式

假设 是一个算术程序,它以 的元素为输入并输出 个元素,总共有 个I/O元素。那么,当且仅当存在系数 使得 可以整除 时, 的输入和输出的有效赋值,其中:

布尔电路

通常情况下一般的通用snark算法使用的是QAP来去表示程序,但如果程序是一些特殊问题,比如输入程序可以表示为布尔电路,那么QAP实现就可以更加简单一点。首先我们来看一下布尔电路的特点:

alt_text

从图中可以看到不管是哪一种的门,最终的输出结果一定是落在[0, 2]区间之内。具体来说:任何一个2输入的二进制门电路 ,其中输入为 ,输出为 ,都可以使用门电路的输入和输出的仿射组合 来指定,当输入输出满足门电路的逻辑规范时,它只能取两个值, 。这导致了一个等效的单一的“平方”约束

SSP

根据上述布尔电路的特点,一般的QAP约束在布尔电路中就转换成了SSP(Square Span Program)约束。我们来看一下SSP的具体定义:

定义(SSP):在域 上的一个方形跨度程序(SSP)是由 个多项式 和一个目标多项式 组成的元组,使得对所有 ,都有 。我们说方形跨度程序SSP的大小为 ,并且度数为 。当且仅当存在 ,使得 能够整除 时,我们称SSP接受输入 ,其中:

我们说SSP校验了布尔电路 ,如果它仅接受那些满足 的输入值

再进一步,我们可以根据SSP而具体的布尔电路构造方形约束系统(Square Constraint System)。我们首先来看一下SCS的定义:

定义SCS: 方形约束系统由一个矩阵 定义。如果满足以下条件

其中 表示Hadamard(逐元素)乘积,那么向量 是此系统的解。我们也将 写为

我们可以看一个具体的例子,比如我们有3个布尔元素分别是 : 对于布尔元素而言,比如说 要么为 0,要么为 1。注意到

这意味着 ,从而推导出 。其他元素也是同理。对于

综合上述内容,一个包括上述导线和门的方形约束程序将采取以下形式:

babysnark

介绍了这么多,终于到babysnark了。babysnark是对布尔电路所构造的一种snark。相比于QAP而言,SSP更简单,所以实现整个snark所需的约束也更少。具体来说一共有两个约束,第一个是SSP约束:

不需要做太多解释,第二个约束是线性约束:

这个和babysnark具体设计有一些关系。 的值是由prover直接计算的,而 的值来自于setup阶段。设置线性约束的目的是确保 确实是由同一线性多项式计算出来的,防止prover作弊,恶意构造 而不是赖在setup所提供的随机challenge构造的 ,最终破坏SSP约束。因为prover最后输出证明的时候同时提供了 在verify阶段添加 是为了防止证明者输出特别恶意构造的 B=YV,所以再做一次线性约束。

babysnark的随机挑战采用的是 的形式,该构造形式的安全保证来自q-DLOG 假设。q-DLOG 假设确保即使敌手可以在多个点上观察到多项式的值,他们也无法从多项式的结构中提取任何信息。

至此,我们对babysnark的原理部分做了详细的探讨。希望通过深入浅出的方式介绍这一简易的snark,能为你的零知识证明学习之旅提供坚实的基石。

Reference

[1] BabySnark do do do

[2] quadratic-arithmetic-programs-from-zero-to-hero

[3] 从零开始学习 zk-SNARK(三)——从程序到多项式的构造

[4] zk-SNARKs: A Gentle Introduction

KZG

KZG 承诺又叫做 KZG10 承诺,是由 Kate, Zaverucha, and Goldberg 三位作者共同提出.

1.多项式表示

多项式 P(x)可以用系数表述,如简单可表示为

,所以对于一个多项式 P(x)可以表示为,其中表示对应位置的系数.

2.Commitment Scheme

2.1 Commit Schemes 过程:

可以把承诺 C(m)理解为一个装着信件 m 的信封

  • Setup 阶段产生一些公共参数
  • Commit 阶段:对消息 m 进行承诺得到 C(m)
  • Open 阶段:打开 C(m)得到 m‘,验证 m是否等于 m’. commit 阶段的 m,在 open 阶段是会暴露的.

2.2 commit Schemes 性质:

  • Hiding:意味着敌手获得承诺 c(m)后无法获得 m 的值

    • computational hiding:对于任意的 PPT 敌手 A.有
    • Perfect hiding:将 A 的计算能力修改为无穷算力,“≤ negl(λ)”替换为 0
  • Binding:是指一个承诺 c(m) 在 Open 阶段打开只会得到 m 而不会得到 m‘.

    • computational Binding
    • perfect binding::将 A 的计算能力修改为无穷算力,“≤ negl(λ)”替换为 0

2.3 Polynomial Commitment Schemes:PCS

多项式承诺 PCS:承诺对象是单变量多项式,:表示所有 degree 最多为 d 的单变量多项式的集合。过程可总结如下图

  • Prover 运行 Commit 算法,将函数 f 与随机数 r 作为输入,为输出.将发送给 Verifier
  • Verifier 发送一个挑战点:即一个函数域 X 中的元素 x
  • Prover 将 x 对应的 f(x)=y,以及 proof 发送给 Verifier. 表明 1.f(x)=y 2.f 属于 F,即 f 的 degree<=d.

其中 Prover 需要计算如下内容

  1. 多项式的承诺 C=[P(x)]
  2. 多项式在 z 点的值,P(z)=y,这很简单
  3. the proof

PCS 有多种,比如 FRI or Dark'20 or Dory'20 .但是 KZG 仍然是目前实践中使用最为广泛的 PCS 方案.其特点如下

  1. 基于 Pairing 实现
  2. Proof size 是常量 (一个椭圆曲线群元素)
  3. 验证时间是常量 (两次 pairing 操作)

其中特性 2 与 3 导致可以将其构造成一个 SNARK 方案.SNARK 的全称是 Succinct Non-interactive Argument of Knowledge:简洁非交互式知识论证.

SNARK 要求 1.size of proof=O(log(d)) 2.time of Verification =O(log(d)),d 为 degree of Polynomial.

进而可以将 KZG 应用在零知识证明系统如 ZK-SNARK 中.

3.计算多项式的承诺 C

在计算之前,首先介绍两个概念

3.1 椭圆曲线(EC)

这里只简单提一下椭圆曲线,更多细节可参考阅读 basic elliptic curve cryptography series.

假设是由椭圆曲线点构成的群,g 是的生成元.

用符号[x]表示.由于椭圆曲线的离散对数难题,给定 g 与[x],但无法逆推出 x.

3.2 Trusted Setup

对多项式进行承诺,需要一个与多项式系数数量一样长的 structured reference string(SRS)。该字符串必须按照指定的方式生成,并提供给任何希望承诺多项式的参与方。生成过程会产生一个秘密值 s,也称为 trapdoor 或者 toxic waste),必须将 s 其丢弃。换句话说,生成参考字符串的任何一方都知道一个信息片段,该信息可以破坏多项式承诺方案的 binding 性质,从而破坏使用该承诺方案的任何证明系统的正确性。生成这样的 SRS 过程被称为可信设置(trusted setup).

设 D 是希望支持承诺的多项式 P(x)的最高次数上界, SRS =.

目前主流是通过 Ceremony 生成 SRS,关于 Ceremony 的详细细节可参考 https://mirror.xyz/privacy-scaling-explorations.eth/naTdx-u7kyirczTLSAnWwH6ZdedfTQu1yCWQj1m_n-E

Ceremony 的思想与 MPC 类似,让 N 名参与者生成自己的秘密,并按顺序将其添加到主秘密中。只要有一个参与者不泄露秘密,那么主秘密就是安全的。主秘密的生成过程被称为 Ceremony.

可进入 https://ceremy.ethereum.org 参与以太坊社区组织的 KZG Ceremony 的生成过程,成为其中一名贡献者!

3.3 combine Trusted Setup and EC

  • Trusted Setup 阶段生成 SRS=(),n 为 P(x)的 degree.然后将 s 丢弃。任何人都可以访问 SRS,但是无法获得 s 本身.

  • 通过 SRS 重新构造多项式 P(x)为[P(s)],而不会暴露 s 与多项式本身

上式中,用秘密 s 替换自变量 X,得到 P(s):因为自变量 x 可以表示为任何值,.这不影响多项式本身.进而得到承诺 C=[P(s)]

4.计算 proof

我们需要 proof 证明 P(z)=v.构造前先引入一些 polynomial math.

P(x)的零点为 m,即 P(m)=0.那么 P(x)一定能整除(x-m),即存在一个商多项式 q(x).使得

想要证明的是 p(z)=v,结合上述 polynomial math.可做如下变换.

p(X)-v=0 when X=z,则 p(X)-y 能整除(X-z) ,即,即.

也把 q(X)称为**"Witness Polynomial"**

对于,不能直接利用这个等式,因为等式中的**s **两方都不知道

直觉上,我们希望直接证明等式 [p(s)-v] = [q(s)*(s-z)]成立,从而完成验证.

等式左边:

承诺仅满足加法同态:,所以[p(s)-v]=[p(s)] -[v]

等式右边:

在验证过程中,验证方会收到证明方发来的[p(s)] ,同时验证方自己可以计算[s-z]=[s]-[z]的值

但是由于椭圆曲线上不满足乘法,即乘法****同态:[p(s)]*[q(s)] =[p(s)*q(s)]

所以等式**[q(s) * (s-z)]= [q(s)]*[s-z]** 并不成立,需要引入配对 pairing

因为椭圆曲线上的运算是一个加法群,而不是一个乘法群,乘法没有被定义。

这里需要强调的是,单个运算结构其实并不区分加法乘法,a o b 这个 o 把它称作成什么都行 只是在有限域上的椭圆曲线点集构成一个加法群,把它称为加法是更符合习惯。 我们区别加法与乘法,比如两种运算的代数结构比如环,域。 因为有两种运算,需要做区分,因为涉及到分配律,谁对谁分配的问题,所以会很明确的区分加法与乘法。

5.Pairing

Pairing is **a bilinear mapping.深入学习Pairing可参考《Pairing for beginners》**这本书,在这里只做简单介绍.

  • bilinear
    • Linearity:对于某些一元函数,如果该函数服从
    • Bilinearity:对于二元函数,Linearity 存在于所有维度中,即

  • bilinear mapping 双线性映射是一个函数,它从两个向量空间的元素产生第三个向量空间的元素,每个参数都是线性的.

5.1 about pairing

配对是⼀种抽象操作。其定义可能会有所不同。 有 Tate 配对、Weil 配对、Ate 配对等等…… 虽然每⼀个都通过不同的操作来定义配对,但是Input与output的格式,pairing 的属性都是固定的.

Input:

output

n 阶乘法群中的整数(或复数)

分别是对称与非对称的 Pairing 形式。在实际中,非对称 Pairing 效率最高。

properties:

  • e(P, Q + R) = e(P,Q) * e(P, R)
  • e(P + S, Q) = e(P,Q) * e(S, Q)
  • (bilinear)
  • e(P, Q) ≠ 1 (non-degeneracy property)

5.2 Pairing examples

1.e(x, y) = 2ˣʸ

例: 请举例在实数域中 e(x, y) = 2ˣʸ 是双线性函数.

  • e(3, 4 + 5) = 2³˙⁹ = 2²⁷
  • e(3, 4) * e(3, 5) = 2³˙⁴ * 2³˙⁵ = 2¹² * 2¹⁵ = 2²⁷.
  • 通过 pairing 证明知道 x² - x - 42 = 0 的解, 然而并不透露这个解的具体数值.

如果 成立, 那么 k 必须为 0 或者目标群的倍数.

如果存在 , 可以确定原始二次方程式成立. 使用双线性性重写方程 .进一步,e(xG, xG) ⋅ e(xG, -G) ⋅ e(G, -42G) = 1.

因此只需要提供 xG 的值. 同时由于椭圆曲线的离散对数问题, 从 xG 反推回 x 是困难的.

2.解决 Diffie-Hellman 难题

3.BLS签名

6.KZG

回到KZG部分

分别是同一椭圆曲线的两个子群.g 是子群的生成元,h 是子群的生成元

生成元的选择通常在 trusted Setup 阶段选择

define pairing e: ,对于秘密 s 也相应有两个分布.即SRS

原来要验证的等式: =>

分布集1: ,对应生成元为g。计算π、C、

分布集2: ,对应生成元为h。计算

验证者验证等式:

简单理解这个等式:

[x]g 与 g^x 表述形式不同,本质上没有什么区别。 a o b =c 如果群运算定义为加法,就使用[x]g 这种形式 如果群运算定义为乘法,就使用 g^x 这种形式

用黑盒来理解这个等式的话,就等价于在群中去验证下面乘法的成立

Verifier 如何进行验算:

  • prover 发送,C,v
  • Verifier 自己选择的 z,根据加法同态,Verifier 可以计算_[s -z]₂=[s]₂ - [z]₂_
  • g,h is public,pairing function is public.

KZG 完整过程:

  1. 通过 Trusted setup, 产生 SRS:[sⁱ]₁, [sⁱ]₂.
  2. Prover 使用_[sⁱ]₁_,对多项式 P(x)进行 commit,得到 C = [p(s)]₁, 发送给 Verifier.
  3. Verifier 选择挑战点_z _∈ {0,...,_p_−1}
  4. Prover 发送 π 、y 给 Verifier:
  5. Verifier 检查等式: _e(π, [s -z]₂) = e(C -[v]₁, H) _ if the equation holds, the verifier accepts the proof if the equation does not hold, the verifier rejects the proof

KZG 分析

对 KZG 的 Corretness Binding hiding 分别分析

  • Corretness

等式左边:

等式右边:

  • hiding

因为椭圆曲线的离散对数难题,敌手拿到[x]无法得到 x.

  • Binding

分析 Binding 前,需要介绍 SDH 假设。

Strong Diffie-Hellman(SDH) 问题定义如下:

给定(q+1)长的元组 作为输入,输出

SDH假设就是不存在多项式时间算法可以不可忽略概率解决 SDH 问题。下面用对称形式的 Pairing 进行分析

后续 pairing 的验证都是“g 的指数上”在进行验证,为了方便起见.省略底数 g,后续的等式都是在指数位置上进行.

反证法,即KZG不满足 binding,那么 open 承诺 C 可以得到值 v 和 v',承诺方必须确定两个不同的值 y 和 y',使得下列等式成立:

因为,假设 , 等式两边同时除以可得:

,即,这说明有人可高效计算出,这违背了SDH假设.

总结:

像之前说的那样,KZG 方案的 Proof size 是常量 (一个椭圆曲线群元素),验证时间也是常量 (两次 pairing 操作),这是其优点.但是其最大缺点是需要一个 Trusted Setup 阶段.

7.Batch-KZG proof:multi proof

上述过程验证了⼀个在单点上求值的多项式。但如果想证明⼀个多项式上在多点上的值,就必须⼀次⼜⼀次地重复同样的协议 (back and forth)。这显然是没有效率的。为了解决这个问题,需要 "批量 "验证多项式上的点。

假设想证明 k 个点上的值:

通过使用拉格朗日多项式插值法,构造一个经过上述 k 个点对的 k-1 次多项式

n+1 个坐标对的形式 可以唯一的恢复出一个多项式

**原多项式 P(x)构造的 I(x)**都经过 k 个点对,所以多项式 P(x)-I(x)=0 在如下点上满足

即多项式能够整除

定义一个 zero polynomial:

则下式成立

定义 kate multiproof for the evaluation of these points:

验证过程如下:

  • Verifier 通过 k 个 points(z,y)计算 Z(x)和 I(x)
  • Verifier 计算
  • Verifier 验证等式是否成

8.KZG in ZK-Rollup

在 zk-rollups 的情况下,想证明发生在 L2 上的一些计算是有效的。简单来讲,发生在 L2 上的计算可通过称为“ witness 生成”的过程表示为二维矩阵。然后可以用多项式列表来表示矩阵 - 每列都可以编码为其自己的一维向量。然后,计算的有效性可以表示为这些多项式之间必须保持的一组数学关系。例如,如果前三列分别由多项式 a(x)、b(x) 以及 c(x) 表示,可能需要关系 a(x)⋅b(x)−c(x)=0 保持。多项式(代表计算)是否满足这些“正确性约束”可通过在一些随机点评估多项式来确定。如果“正确性约束”在这些随机点上得到了具体的满足,则一名验证者可以非常高的概率断言计算是正确的。

很自然地看到像 KZG 这样的多项式承诺方案,是如何直接插入到这个范式中的:rollup 将 commit to 一组多项式,它们一起代表计算。 然后,验证者可要求对一些随机点进行评估,以检查正确性约束是否成立,从而验证多项式表示的计算是否有效。

最后感谢@Kurt-Pan的指导与建议

参考文章

Understanding KZG10 Polynomial Commitments (taoa.io)

Kate Commitments: A Primer - HackMD

Dankrad Feist's kzg commitment post

https://blog.subspace.network/kzg-polynomial-commitments-cd64af8ec868

Understanding KZG10 Polynomial Commitments

Committing to lunch (taoa.io)

book:Proof,argument and zero knowledge

KZG原始论文

Lecture1:Introduction to Zero knowledge Interactive Proofs

NP

NP Proof:

NP-proofs 属于可高效验证的 proofs.其中要求

1.Witness 的长度应当是 statement x 的长度的多项式表示.

2.Verifier 时间是 x 长度的多项式函数表示

NP proofs 例子

上述例子都可以用一种通用的语言关系 L 来表示

更具体而言,上述都是 NP 问题:(简单来说,求解困难,但是验证高效的问题)

  • P NP NPC
    • P 问题:指能在多项式时间求解出的问题.如 2SAT,欧拉路径,PATH 问题
    • NP:Nondeterministic polynominal(非确定性多项式) 一个问题不能确定是否能够在多项式时间内找到一个解。但若给出一个解,能在多项式时间内证明这个解是否正确 .如果找到一个解,那么 NP 问题就变成了 P 问题,所以 P∈NP 类 注:NP 问题不能理解为非 P 问题 著名的 NP 类问题:旅行家推销问题(TSP)。即有一个推销员,要到 n 个城市推销商品,他要找出一个包含所有 n 个城市的环路,这个环路路径小于 a。如果单纯的用枚举法来列举的话会有(n-1)! 种,已经不是多项式时间的算法了,阶乘比多项式复杂。假设有人猜几次就猜中了一条小于长度 a 的路径,TSP 问题解决了。可是,人们不可能每次都猜的那么准。所以说,这是一个 NP 类问题。也就是,我们能在多项式的时间内验证并得出问题的正确解,可是我们却不知道(非不存在)该问题是否存在一个多项式时间的算法能解决 NP****问题的本质是单向性,不可快速求解,但是能够快速验证
    • NPC: 规约:问题 A 可以转化为问题 B,对于难度而言,问题 B 比问题 A 要困难。规约具有传递性:A 规约至 B,B 规约至 C,那么 A 规约至 C。一直规约下便会得到 NPC 问题 所有的 NP 问题都可以约化成 NPC 问题。只要解决了这个问题,那么所有的 NP 问题都解决了。NPC 需要满足两个条件.1:是一个 NP 问题 2:所有的 NP 问题都可以约化到它。如 SAT 问题、HAMPATH 问题都属于 NPC 问题.
    • NP-hard 问题 它满足 NPC 问题定义的第二条但不一定要满足第一条(就是说,NP-Hard 问题要比 NPC 问题的范围广,NP-Hard 问题没有限定属于 NP),即所有的 NP 问题都能约化到它,但是它不一定是一个 NP 问题
    • P NP NPC NP-hard 关系

NP Language 定义:

给定二元关系,记语言 L(R)为集合.称一 个语言 L(R) 是 NP 语言当如下两个条件成立:

  • |w|=poly(|x|)
  • 给定任意的 x、w, 存在多项式时间算法能够高效判定 R(x, w) =? 1

注:此时还与零知识无关

考虑有没有其他方式,比如在大素数分解 N=PQ 中,不暴露 P 或 Q 的值让验证者相信这些类型的定理

ZKP

引入交互随机

交互

验证者不再被动地阅读证明,相反验证者会与证明者进行重要的交互。

Prover 与 Verifier 之间进行多项式步骤的****交互

随机

Verifier 不再是一个确定性的算法

Verifier 的问题通过抛硬币的方式,即问题在某种程度上是不可预测的

随机性的本质是接受小概率的错误,但这种概率应当被量化,比如小于某一个可忽略函数.

例子

参考郭宇老师的初识「零知识」与「证明」—— 探索零知识证明系列(一):地图三染色问题

Definitions of Zeroknowledge interactive Proof

1.Interactive Proof system for a Language L

可忽略函数:一个比任何多项式函数分之一增长都慢的函数

Zero knowledge

谈论零知识前,需要引入模拟器的概念.

这里参考郭宇老师--从「模拟」理解零知识证明:平行宇宙与时光倒流---探索零知识证明系列(二),而不使用课程的例子.

模拟器:Simulator

简单来说

**现实世界/视图:**是一个概率分布.这个空间中的点都是证明者和验证者之间交互的所有可能历史加上验证者的硬币投掷。

理想世界/视图:也是一个概率分布.但理想世界/视图需要是算法可构造的,由 Simulator 负责构造.

理想世界/视图与现实世界/视图的差异在于:理想世界/视图没有知识,现实世界/视图拥有知识.

对于一个多项式时间的区分器,它从上述分布中选择一个样本.如果区分器不能区分这个样本来自哪个分布,即来自哪个世界/视图,我们说这样的交互协议是零知识的.

因为理想世界/视图中的 Zlice 是没有任何知识,而且她和真实世界/视图中的 Alice 不可区分.

或者说区分成功的概率不大于 0.5.那么这两个分布在计算上是无法区分的。

Simulator 是怎么做到这一点的? Simulator 能够而是因为它可以"倒序"生成 视图/世界,即 Simulator 可以先随机选择一个挑战,然后基于这个挑战生成证明的一部分。这样生成的视图在统计上与真实的视图无法区分。

计算不可区分定义:

这里给出更一般的计算不可区分定义

这里的安全参数 k 表示某个计算困难问题的困难问题实例输入大小。每个困难问题都有一个界,当输入大小超过这个界时,我们就认为这个问题是计算困难的。--具体可参考刘巍然老师的回答

Zero Knowledge Define:

Flavor of Zero Knowledge

零知识分类

这里不给出形式化定义,简单来说即

两个随机变量的分布是计算不可区分的,也就是任何多项式时间的随机敌手都无法区分这两个分布,就称这个证明系统是计算零知识 (Computationally Zero-Knowledge)

两个随机变量的分布是统计不可区分的,也就是它们的统计距离 (Statistical Distance) 可忽略,就称这个证明系统是统计零知识 (Statistically Zero-Knowledge) 的;

如果统计距离就是 0,又叫做完美零知识 (Perfect Zero-Knowledge) 的;

Proof of Knowledge

一个 Proof System 是 POK 的,需满足以下定义

需要引入抽取器,抽取器具有时光回溯的能力.

抽取器 在理想世界中,通过时间倒流的超能力,把 Prover 的知识完整地抽取出来

注:抽取器可以提取出 witness,不是因为它具有无限的计算能力,而是因为它能与证明者进行多次交互。在每一轮的交互中,抽取器都会选择一个新的挑战,然后记录下 Prover 的回应。

这就保证了一个没有知识的 Prover 是无法让抽取器达成目标,从而证明了可靠性。

抽取器只能从能成功完成证明的证明者那里提取出(witness)。如果一个 Prover 不能成功完成证明,那么他可能并不知道一个有效的 witness,因此抽取器无法从 Prover 那提取出 witness。

把这样一个依靠采用抽取器来证明可靠性的证明系统被称为 Proof of Knowledge

注:不是所有的可靠性都必须要求存在抽取器算法

这里可参考郭宇老师:探索零知识证明系列(三):寻找知识

The First Application:Identity Theft

Alice 想通过互联网证明向 Bob 证明其就是 Alice,比如 Bob 是亚马逊的.

思想是:将证明 Alice 的身份转为 Alice 证明一道特定的难题,谁知道这个难题的答案谁就是 Alice.

NP and Zero Knowledge Interactive Proof

结论:如果单向函数(简单来说 is easy to compute on every input, but hard to invert given the image of a random input,具体参考 wiki)存在,那么每种 NP 语言都具有零知识交互证明。

先引入承诺的概念

承诺

承诺性质

  • Hiding:意味着敌手获得承诺 c(m)后无法获得 m 的值

    • computational hiding:对于任意的 PPT 敌手 A.有
    • Perfect hiding:将 A 的计算能力修改为无穷算力,“≤ negl(λ)”替换为 0
  • Binding:是指一个承诺 c(m) 在 Open 阶段打开只会为一个值 m 而不会得到 m‘.

    • computational Binding
    • perfect binding::将 A 的计算能力修改为无穷算力,“≤ negl(λ)”替换为 0

举例:地图三染色问题

地图的三染色问题是一个 NP 完全问题,即 NPC 问题.

生动易懂的例子仍然可以参考郭宇老师的初识「零知识」与「证明」—— 探索零知识证明系列(一):

过程总结如下:

性质如下

完整性:一个诚实的 P 总是可以说服验证者接受。因为无论 Vr 需要哪条边,P 总是可以正确地给出符合规则的上色方案。 总是接受

**健全性:**无论 恶意的 P *做什么,都会有一条边缘的颜色不正确。当 V 选择随机边时,实际上选中错误的概率是 1/E,成功骗过 P 的概率是(1-1/E).随着重复的次数增多,成功的概率变为,呈指数级降低。概率接受

零知识性:很容易能够看出来,整个过程验证者得到了很多信息,但是这些信息并不会帮助验证者获取地图三染色问题答案的知识.但形式化证明很麻烦。

模拟器 不知道染色答案,它可以提前确定诚实 V 会询问的边 E‘ 模拟器 可以将整个图都涂成一样的颜色,除了 E’的两端 a,b 会被涂成两种不同的颜色. 对于模拟世界/视图:它会输出一堆承诺,由于承诺的性质,这些承诺不会给计算能力有限区分器 提供任何信息.然后 区分器 随机选择一条边 E,由于模拟器能够提前知道区分器随机选择哪条边.所以 E=E‘. 打开 E/E‘的承诺以后,V 会发现 a,b 是不同的颜色.模拟器 成功完成该轮挑战 但实际上,模拟器 并不知道染色答案,但是 V 每次打开 E 的承诺,对应的 a,b 端点颜色又确实是不一样的.所以真实世界/视图模拟世界/视图在计算上是无法区分的.

zk 的应用

法律 隐私 生物 DNA 甚至是核裁军

复杂性理论(Complexity Theory)

Interactive Proof

  • BPP:复杂类 BPP 在多项式时间内对概率图灵机解出的问题的集合, 并且对所有的输入,输出结果有错误的概率在 1/3 之内

  • IP:交互式证明

    • 是一种包含了两个参与者(证明者和验证者)的验证系统,其中证明者试图通过多轮对话来说服验证者某个复杂陈述的真实性。
    • 定义
      • 设⟨A, B⟩ 为一对交互式图灵机. 记 ⟨A(y), B(z)⟩(x) 为在 A、B 的随机输入带均匀独立选取, 公共输入为 x, A 的辅助输入为 y, B 的辅助输入为 z 时, 图灵机 B 与图灵机 A 交互后输出的随机变量.
      • 给定二元关系 R 及其对应语言 L(R), 则针对该语言的 IPS(interactive proof systems)是用符号⟨P(y),V(z)⟩表示 .其中,图灵机 P 与 P*可以是无穷算力 V 是 PPT 的.则 IPS 满足两个性质
        • 完备性 (completeness): 对于任意的 x ∈ L(R), 存在 y, 使得对于任意的 . 完美完备性 (perfect completeness) 是指上述概率等 于 1.
        • 可靠性 (soundness): 对于任意的 x ∈/ L(R), 任意的恶意证明者 P*, 任意的.
  • IP 与 NP 的关系

    • IP 类可以看作是经典复杂类 NP 的交互式****随机变体,所有的 NP 问题也都可以在 IP 中找到解决方案,因此 IP 是 NP 的超集。
    • 在 IP 系统中,Prover 和 Verifier 之间有多轮的交互,而在 NP 问题中,证明(或解决方案)一旦生成,就可以独立地被验证,不需要进一步的交互。如果不允许交互,但允许验证者抛掷随机硬币并以小概率接受错误的证明,那么得到的复杂性类被称为 Merlin-Arthur(MA)

    这再次说明了 IP 强大的关键在于随机性交互的结合

    • Prover 在 IP 中并没有时间复杂度的限制,可以是全知的,这意味着它可能运行在超出概率多项式时间的时间复杂度。它能够进行任意复杂的计算来构建它的证明,只要这个证明能够在多项式时间内被验证
    • Verifier 在 IP 中必须运行在概率多项式时间(probabilistic polynomial time)内,即必须在多项式时间内完成计算

Private Coins Model

Interactive Proofs with Public Coins Model

“We can formulate a decision problem under uncertainty as a new sort of game, in which one opponent is ‘disinterested’ and plays at random, while the other tries to pick a strategy which maximizes the probability of winning – a ‘game against Nature’.” ---Christos Papadimitriou. Games Against Nature. FOCS 1983.

如果不允许交互,但允许验证者抛掷随机硬币并以小概率接受错误的证明,那么得到的复杂性类被称为 Merlin-Arthur(MA)

AM 与 MA

  • Arthur-Merlin Protocol

协议中的两个参与者分别称为 Arthur 和 Merlin,基本假设是 Arthur 是配备随机数生成设备的标准计算机(或验证器),而 Merlin 实际上是具有无限计算能力的预言机(也称为证明者)。不过,Merlin 不一定是诚实的,所以亚瑟必须分析 Merlin 在回答亚瑟的询问时提供的信息,并自行决定问题。如果在这个协议中,每当答案是“是”时,Merlin 有一系列的回应,会导致 Arthur 在至少 2/3 的情况下接受,那么问题被认为是可以通过这个协议解决的。而如果答案是“否”,那么 Arthur 永远不会在超过 1/3 的情况下接受。

  • MA

1-message protocol

Merlin 向 Arthur 发送消息,然后 Arthur 通过运行概率多项式时间计算来决定是否接受。(这类似于基于验证器的 NP 定义,唯一的区别是 Arthur 在这里被允许使用随机性。Merlin 在这个协议中无法访问 Arthur 的硬币抛掷,因为它是一个单消息协议,Arthur 只有在收到 Merlin 的信息后才会抛硬币。

从形式上讲如果存在多项式时间确定性图灵机 M 和多项式 p,q 使得对于长度 n = |x| 的每个输入字符串 x,

  • AM

复杂度类 AM(或 AM[2]或 AM[K])可以通过具有两条/K 条消息的 Arthur-Merlin 协议在多项式时间内决定。只有 1/2/K 个查询/响应对:Arthur 随机抛出一些硬币并将他所有抛硬币的结果发送给 Merlin,Merlin 用所谓的证明做出回应,Arthur 确定性地验证证明。

在这个协议中,Arthur 只被允许将抛硬币的结果发送给 Merlin,在最后阶段,Arthur 必须只使用他之前生成的随机抛硬币和 Merlin 的信息来决定是接受还是拒绝

从形式上讲如果存在多项式时间确定性图灵机 M 和多项式 p,q 使得对于长度 n = |x| 的每个输入字符串 x

简单总结:

摘自啊咪咪小熊--- MA 就是最简单的 M 给 A 发完就结束了,A 自己决定是否接受,就是非交互式的(和 NP 的区别就是 Verifier 可以用随机数)。 AM 就是 A 先给 M 发一个随机数,然后 M 再给 A 回复,然后 A 再决定是否接受,就是交互式的

IP = PSPACE 定理

任何可以在多项式空间内解决的问题都可以通过交互式证明的方式来解决。

具体可参照,这个假设的重要性在于它将两个看似不同的计算模型联系在了一起.

其中 PSPACE 简单理解为是比 NP 要大得多的语言类

MIP

MIP 类似于 IP,只是有多个证明者,并且假设这些证明者不会相互共享关于他们从验证者那里收到什么挑战的信息。MIP 的一个比喻是在审讯犯罪嫌疑人之前将多个犯罪嫌疑人放在不同的房间里,看看他们是否能保持他们的故事直截了当。对 MIP 的研究表明,如果一个人将证明者锁在不同的房间里,然后分别审问他们,他们可以说服审问者做出比一起接受审问要复杂得多的陈述。

Non Interactive Proof

通过 Fiat-Shamir 转换将 Interactive Proof 变为 Non-Interactive Proof

注:

Lecture2:Introduction to Modern SNARKs

Overview about zk-SNARK

Why commercial so much

历史可追溯至 1991 年的一篇论文[Babai-Fortnow-Levin-Szegedy'91]

Zk-snark application

  • blockchian

    • 外包计算:
      • 扩容(zkRollup):离线服务处理交易批次,L1 链验证一个简短的证明来证明该批次内的交易都是有效的,而无需分别验证每笔交易
      • 区块链桥接(zkBridge):将资产从源链转移到目标链.源链的共识协议同意锁定某些资产,以便在另一个链中使用。通过验证 zksnark 生成关于源链到目标链的共识状态的简短证明,而无需验证源链共识的整个过程. 在上述两个例子中,非交互式证明非常重要。因为证明需要由大量区块链验证者进行验证
    • 隐私性:需要零知识性
      • 公共区块链上的隐私交易:ZKP 在不泄漏交易信息的情况下证明一个私人交易是有效的。例子:TornadoCash、ZCash、Ironfish、Aleo。
      • 合规性:证明一个私人交易符合银行法规(例如 Espresso) 证明一个交易所具有偿付能力而无需泄漏拥有资产情况(例如 Raposa)。
  • Non-blockchain:打击虚假信息, [Kang-Hashimoto-Stoica-Sun'22]

当阅读报纸文章时,文章通常会插有图片,但图片可能与文章内容完全无关甚至带有误导性质.

解决方案是 C2PA 标准,其代表内容来源和真实性,目标是为报纸文章中的图像提供真实的出处。

工作原理:在每台相机中嵌入一个由制造商嵌入的密钥。该密钥无法从相机中提取出

每次符合 C2PA 标准的相机拍摄照片时,会对照片以及与该照片关联的所有元数据进行签名,比如拍摄照片的位置和时间戳,然后嵌入到相机生成的原始图像数据中.

当图像嵌入文章并发送给读者时,读者可以简单地验证图像上的签名,并向读者显示元数据、位置和时间戳.

但 C2PA 标准有一个后处理的问题,即这些相机可捕捉非常高分辨率的图像。

但是为避免将图像发送给最终用户时浪费太多带宽,发送时会对图像采样.这意味着图像可能被重新调整为更低的分辨率,会被裁剪,会进行灰度化处理等.当将经过处理的照片发送到笔记本电脑时,笔记本电脑无法再验证图像上的 C2PA 签名。即必须拥有原始图像数据,否则无法验证签名。

所以问题就是:签名存在,但读者没有获得原始图像数据,因此无法验证签名是否有效。

采用 zk-snark 解决,思想:在图片被编辑前,为该图片生成一个 zk-snark proof.

Operation 表示应用于该照片的操作列表:例如缩小尺寸、灰度化、裁剪

读者验证 zk-SNARK 证明,如果有效则将向用户显示元数据.

Define zksnark

首先介绍电路部分

Arithmetic Circuits

Fix a finite field F={0,1,…,p-1} for some prime p>2

Arithmetic Circuits 是一个函数,它接受有限域中元素作为输入并产生有限域中的元素作为输出。

它由若干域上的加法门和乘法门组成. 电路的大小=电路中门的数量,如上图 |C|=3.

电路可满足问题 (circuit satisfiability problem, C-SAT) 是指给定电路 C、 电路的部分输入 x (x 可为空) 和电路输出 y, 判断是否存在证据 w (电路的另一部分输入, 视为秘密输 入) 使得 C(x, w) = y.

布尔电路(Boolean circuit)是算术电路的子类, 其仅有与门、异或门等布尔逻辑门, 变量取值仅为 0 或 1. 可以证明, 通过增加常数级别的电路门和深度, 任何布尔电路都可以转换为算术电路

Valiant's theorem:所有多项式时间可计算的函数都可以通过多项式大小的算术电路来表示

仅使用加法、乘法和减法,就可以实现 SHA256 函数,大约需要 20,000 个门才能完成,

Structured vs. unstructured circuits

非结构化电路:电路中有一堆门,而电线只是按照开发人员想要的方式去连接门

结构化电路:电路本身实际上是分层构建的,其中有一个固定的算术电路 M。

          输入从底部进入,重复应用M,最后计算输出。

M 有时被称为虚拟机

NARK: Non-interactive ARgument of Knowledge

NARK 代表非交互式知识论证,其应用于算数电路

电路的输入:公共 statement x 与秘密 w; 输出仍是 F 中的元素

NARK 会经过一个 Setup 阶段即预处理,Setup 将电路的描述作为输入,产生一些公共参数作为输出.

其中一部分参数与 Prover 相关,称为 PP.另外一些参数与 Verifier 相关,称为 VP.

Prover 通过 PP,x,w 作为输入,产生一个 proof .表明 C(x,w)=0.

Verifier 通过 PP,x 作为输入,对该 proof 进行验证.

整个过程 Prover 与 Verifier 并没有交互.

Define NARK Normallly

Properties of NARK

Knowledge soundness

Verifier 接受 w,表示 Prover know w.st C(x,w)=0 成立.如何理解 konw 呢,这就是第一节提到的提取器的概念

非正式来说,w 能够以某种方式从 Prover 中提取到 w,则表示 Prover know w.

正式定义则如上图.对于多项式时间对手 A 试图充当恶意证明者,在不知道 statement 对应的 w 情况下通过 Verifier 的验证,其中 A 分为两个算法,A0 与 A1.过程如下

  1. 生成全局参数 gp
  2. 将全局参数提供给第一个敌手算法 A0,敌手将生成一些内部状态 st,伪造证明的电路 C 和 statement x
  3. 生成 PP 与 VP
  4. 将 PP,C,x 作为输入运行算法 A1,A1 输出一个 proof

当将这个 proof 与 statement x 一起提供给 Verifier 时,验证者将以百万分之一的概率接受。如果这是真的,那么应该存在一种有效的提取算法 E 并且该提取器 E 将按如下方式工作。

  1. 生成全局参数 gp
  2. 将全局参数提供给第一个敌手算法 A0,敌手将生成一些内部状态 st,伪造证明的电路 C 和 statement x
  3. E 会以某种方式与算法 A1(作为 Oracle)进行交互,然后提取出 w.

提取的 w 满足 C(x,w)=0,概率大约是百万分之一减去一些可以忽略不计的值 等等。

总结:如果对手 A 能够说服 Verifier 它知道某些 C,statement 和对应的 w,

那么就有一个提取器可以与该对手 A 交互并实际上从 A 中提取 w,使得 C (x,w)= 0。

trivial NARK:即 proof 的情况.不满足零知识性,但满足前两个属性.Verifier 可以根据 proof 与 x 重新运行电路 验证 C(x,w)=0 是否成立

SNARK:Succinct Non-interactive ARgument of Knowledge

succinct preprocessing NARK

SNARK 是算法 S、P、V 的三元组,与 NARK 中一样,只是对 proof 提出了额外的要求

证明者生成的证明必须很短,特别是其大小必须是 w 大小的次线性。

证明也应该能够快速验证,这意味着 Verifier 的运行时间应该与电路大小呈次线性关系。

因此,验证者不能简单地重新运行电路 C,但它必须与 x 呈线性关系,因为 Verifier 必须按顺序读取 x

所以 time(V)在 x 上是线性的,但在电路 C 的大小上必须是次线性的。

strongly succinct preprocessing NARK

实践中的 SNARK 实际上会非常简洁(strongly succinct)。

strongly succinct:意味着

  • proof 不仅是 w 大小的次线性,证明长度必须是电路大小的对数关系.使证明与电路相比非常小!
  • 验证证明的时间与 x 的大小成线性,且最多是电路规模的对数关系。

意味着 Verifier 没有时间读取整个电路,也就是 Verifier 甚至不知道电路 C 是什么.也就无法验证一个语句

这就是为什么需要公共参数的原因,它为 Verifier 提供电路的 vp 摘要,以便在 log(∣C∣)内足以运行验证。

ZK-SNARK 就是零知识的 SNARK

像 NARK 一样,考虑一个 trival SNARK 的情况,如上图所示,发现 a trival SNARK 并不满足一个 SNARK 定义.

Preprocessing Setup

Setup 阶段读取整个电路 C,然后输出电路 C 的摘要-一些公共参数,包括 Prover 会用到的 PP 与 Verifier 会用到的 VP.

Setup 阶段通常会采用一些随机位 r 用于生成参数的过程,可分为以下几类

  • Trusted setup per circuit:每一个电路都需要重新执行一次 Setup 过程.随机数 r 非常重要,应当保证 Setup 阶段后 r 被销毁(可信),否则其将能够伪造 proof.

  • Trusted and universal setup:将 Setup 分为两个阶段

    • :是个一次性的算法,产生全局参数 gp.该阶段完成后,r 就被销毁.所以 init 阶段需要是可信的,但该阶段可以用于很多电路.
    • :是一个确定性算法,为证明者和验证者生成参数。任何人都可以运行该算法并验证参数是否正确生成。
  • Transparent setup:不需要任何秘密值,因此任何人都可以验证它是否正确运行,并且不需要运行可信设置.比如 STRAK 协议

Building an efficient SNAKR

一个通用的构建 SNARK 的范例,包含两步或者说两个组件.functional Commitment Scheme 与 Interactive oracle proof.

Commitment 方案是一个加密对象,这意味着它的安全性取决于某些密码学假设。

IOP 交互式预言机证明实际上是一个信息论对象,可以在没有任何底层假设的情况下无条件地证明 IOP 的安全性

Commitment 承诺

简单回顾

承诺性质

  • Hiding:意味着敌手获得承诺 c(m)后无法获得 m 的值

    • computational hiding:对于任意的 PPT 敌手 A.有
    • Perfect hiding:将 A 的计算能力修改为无穷算力,“≤ negl(λ)”替换为 0
  • Binding:是指一个承诺 c(m) 在 Open 阶段打开只会为一个值 m 而不会得到 m‘.

    • computational Binding
    • perfect binding::将 A 的计算能力修改为无穷算力,“≤ negl(λ)”替换为 0

有一个使用哈希函数的标准承诺构造。哈希函数 H:M×R→C,其中

  • commit(m,r)=H(m,r)
  • verify(m,com,r)=accept if com=H(m,r)

Commitment to a Function

  • 选择一个函数族: F={f:X_→Y}. f 表示从集合 x 到集合 y 的函数
  • Prover 运行 Commit 算法,将函数 f 与随机数 r 作为输入, 为输出.f 可以表示为一个电路 C,一个 C 程序等。

然后将 发送给 Verifier

  • Verifier 可以发送回一个函数域中的元素 x
  • Prover 将 x 对应的 f(x)=y,以及 proof 发送给 Verifier. Proof 表明 1.f(x)=y 2.f 属于 F

形式上讲, Function Commitment Scheme 由以下定义:

  • setup_()→gp 输出公共参数 gp

  • commit(gp,f,r)→ 用随机数r∈R 承诺 f∈F

    • 构建 SNARK,必须满足 Binding
    • 对于 hiding, 构建 SNARK 并非必须满足, 但当构建 zk-SNARK 需要满足该属性
  • eval(P,V) :对于给定和 x∈X,y∈Y :

    • Prover(gp,f,x,y,r)→π:生成一个简短的证明
    • V(gp, ,x,y,π)→accept or reject
    • 事实上,Prover 与 Verifier 之间的 eval 算法是对以下关系的(zk)SNARK 证明: 1.f(x)=y 2. commit(pp,f,r)= 3. f∈F
Examples of functional commitments

  • 多项式承诺:承诺对象是单变量多项式, :表示所有次数最多为 d 的单变量多项式的集合。

  • 多线性承诺:承诺为多线性多项式,其中 :表示是 k 个变量的所有多线性多项式的集合,每个变量的次数最多为 1。

    • 多线性多项式示例:
  • 向量承诺:承诺对象是一个向量, 。能够打开该向量中的任何特定单元格。在给定索引 i 的情况下,证明索引 i 处的该函数值 .

    • 向量承诺方案实例:默克尔树(Merkle tree)
  • 内积承诺:承诺一个向量 ,并定义一个函数 ,该函数接受另外一个向量 v 作为输入,并且输出两个向量的内积(u,v).

对于这 4 个承诺方案,可以从中任意一个基础上构建获得剩余承诺方案。

Polynomial Commitment Scheme

Prover 需要对多项式 承诺.Prover 试图说服 Verifier , 满足

  1. 1.f(u)=v ,其中 u,v∈Fp 且公开可见
  2. f 的 degree≤d.

我们希望证明是一个 SNARK,那么证明大小和验证时间应该是 ,下面是一些 PCS 的实现机制

  • Using bilinear groups: KZG'10 (trusted setup,也是实际中使用最多的), Dory'20 (transparent,相比 kzg 慢)
  • 仅使用哈希函数:基于 FRI(long eval proofs)
  • 仅使用常规的椭圆曲线,不需要额外的结构: Bulletproofs (short proof, but verifier time is O(d))
  • Using groups of unknown order: Dark'20(慢,未获得太多关注)

考虑 travial 的 PCS 情况,用系数表示的方式表示多项式 f, ,过程如下

  • commit(f,r)=

  • eval_ 将按如下方式完成:

    • Prover 将 π = 发送给 Verifier
    • verifier 从系数重构 f ,并检查是否 f(u)=v.

很明显这不符合 SNARK 的要求,因为证明大小和验证时间与 d 是线性关系的,而不是 O(log d)

Polynomial is Zero

这是 SNARK 的重要组成部分,也是使 SNARK 成为可能的重要原因

考虑最多为_d_ 次的非零多项式

:从有限域选择一个随机元素 r ,f(r)=0 的概率为 d/p

这是因为 f 最多有 d 个根,r 是从大小 p 的域 中随机选择的.r 命中 d 个根的概率为 d/p.

考虑当 p 远远大于 d 的情况下,比如 , d/p 可以忽略不计.这意味着当 ,Verifier 有着非常高的概率相信多项式在所有点上都为 0.

判断一个多项式是否为 0,只需一个随机点进行评估,并检查评估值是否为零即可。

对应[Schwartz-Zippel-DeMillo-Lipton]定理.该定理也适用于多元多项式,将 d 理解为 f 的总 degree 之合即可.比如

Two Polynomials are equation

如果 ,那么 f=g 的概率非常高.

下面给出判断两个多项式是否相等的交互式协议

  1. V 从 Fp 中随机选择一个随机数 r,将 r 发送给 P
  2. P 根据发来的 r,分别计算 f(r)与 g(r)的值为 y 与 y‘.将 y 与 y’以及对应的 proof 给 V
  3. V 首先检查 proof 是否有效,然后检查 y 是否等于 y‘

通过承诺与 F-S 转换,将上述协议转为 SNARK 方案.

  • 上图是一个 SNARK 方案,当

    • d/p 可忽略
    • 哈希函数 H 作为一个 Oracle,即 H 可自行获得随机质询,然后计算对对应的响应,并将响应发送给 P
  • 该 SNARK 中,Statement x 是 f 与 g 对应的承诺 ;witness w 是 f 与 g 本身.

  • F-S 转化为非交互式 SNARK:P 通过 H(x)获得随机数 r,不再需要 V 发送 r.因为 V 同样能拿着 x 询问 Oracle,获得 r.

  • 但这不是一个 zk-snark,因为 V 可以学习到多项式 f,g 在 r 处的值 y 与 y‘.

Inner product argument

证明者通过内积论证可利用循环****递归的方式证明他拥有两个公开向量承诺的消息, 且这两个消息的内积等于某个公开值. 对于长度为 n 的消息向量, 内积论证的通信复 杂度为 O(log n).

Prover 可向 Verifier 证明对于公共输入 和公开标量 z∈Zq

P 拥有向量 a、b,满足 则 statement 为

,其中向量 a,b 为 witness,g,h,A,B 为公共输入

内积论证的核心思想是将针对 n 长向量的 statement 根据 V 的随机挑战 c 归约为对 n/2 长向量的等价 statement,

在向量不断缩减至为标量后, P 只需要直接发送标量即可.

约定一些符号

  • 1.固定群的生成元 g 后, 记 为 [r], 令 n ∈ N, 记 ,[S]同理
  • 设 g, h 的生成方式为 ,
  • 对于 n 为偶数的向量 (不是偶数可填充), 记

过程如下

  1. 首先基于 V 的随机挑战 c 构造长度一半于原密钥长度的承诺密钥, 即
  2. 为防止 P 利用新的承诺密钥 [r′] 作恶, P 需在挑战阶段之前发送部分承诺值 . 此时新证据为
  3. P 和 V 计算新承诺:
  4. 对于承诺密钥 [s]、承诺 B 和秘密输入 b, 利用挑战 c 的逆 构造对应的承诺密钥 [s′]、新证据 b′ 和承诺值 B′ ,即
  5. 对于 z,P 需在挑战阶段前构造
  6. 更新后的
  7. 归约后的新陈述为
  8. .....递归规约

IOP:Interactive Oracle Proof

F -IOP 的目标是将 f∈F 的承诺转为通用电路的 SNARK。例如,对于一个多项式函数族 ,使用 F -IOP,可以将其转换为任何电路大小为 ∣C∣<d 的 SNARK。

Definition:C(x,w) 是某些算术电路。 .F -IOP 是一个证明系统,用于证明 ∃w:C(x,w)=0 **

Setup: S(C)→(pp,vp),其中 是函数的 Oracle.即 vp 可以理解为 V 可查询的一堆 Oracle,V 可以要求某个给定值显示函数结果,过程如下

  • P 首先发送函数 f1 的 Oracle 。V 稍后可以在其选择的任何点对 f 进行评估.在实际中,
  • V 从 Fp 中随机选择 r1 发送给 P.
  • 重复 Step1 and Step2 t-1 轮
  • P 最后发送 ft 的 Oracle
  • V 开始验证,验证过程 V 可以访问 P 给出的所有 Oracle,以及所有生成的随机数 r 和公共输入 x

Properties of IOP

  • Completeness:

  • Knowledge Soundness:在没有 w 的情况下,恶意 P 无法让 V 相信他知道一个 w,使得 C(x,w)=0

    • Extractor 可以访问 statement x 与函数 本身,因为对于这些函数本身的承诺就是一个 SNARK,所以 Ectractor 可以从 提取 f 本身,进而提取出 w.
  • Zeroknowledge :可不满足

Example of IOP

Polynomial IOP for claim ,用电路 C 去表示该关系: ,过程如下

  1. P 分别计算两个多项式 ,V 也可计算 g(Z),因为 X 是公共的
  2. P 计算一个商多项式 ,只有在 g 包含所有 f 根的情况下,q 才是一个多项式,即X⊆W.举个例子

比如 X={1,2},W={1,2,4}, ,只有X⊆W,q 才是一个有限域中的有效多项式

  1. P 发送 给 V
  2. V 发送一个随机数 r 给 P,虽然 P 不会用到 r,但仍然发送,这表明 r 是一个公共值
  3. V 查询 在 r 点的值,记做 w 与 q‘.计算 g(r).验证 g(r)*q’=w 是否成立

当我们设计 IOP 时,我们所要做的就是设计 P 向 V 发送哪些 Oracle,然后 V 在哪里查询这些 Oracle。

实际中,可以通过多项式承诺方案来实例化 IOP,其中这些 Oracle 被来自 P 的承诺所取代,查询动作基本上通过将查询点发送给 P 来取代,P 进行评估并发回评估正确完成的证明。然后 V 可以决定是否接受或拒绝最终的证明。

IOP + 相应的 Polynomial 方案构造 SNARK

Snarks in Practice

ZK-learning lecture 12:ZK- EVM

Background and motivation

The diagram of Layer1 blockchain

区块链简单介绍:

区块链网络由许多节点组成,通常有大量的节点用于指定,它们通过 P2P 网络互连,所有节点保持与上图红框显示的相同状态.这是一个类似于共享账本的数据库,因此可以将余额或者一些程序代码放在这里.然后使用名为 Merkle tree 的数据结构将所有这些信息存储在列表中.从而得到一个状态路由.

然后对状态路由取摘要来代表所有的状态.每个节点都需要维护相同的数据库.此外节点还将运行称为 EVM 的相同软件进行一些计算并更新状态路由,

区块链也称为 world computer 这个词,因为任何人都可以使用它来运行任何接近去中心化的程序,而运行在区块链之上的程序称为智能合约,因此 evm 将从节点计算机加载 merkle 树叶子结点中的数据到 Storage 中重写这棵树并获得新的状态路由

发送交易:

用户发送交易至区块链中,交易会在 p2p 网络中传播,通过共识算法在每个时隙中选择一个提案, 这个提案将把它收到的许多交易打包到一个块中,同时以交易作为输入运行 evm 并更新状态路由,然后出块.在看到这个块被提交后,网络中的其他节点将下载这个块并重新通过 EVM 执行该块内的交易,就状态路由达成共识.这样始终维护相同的数据库.

Layer1 特点:

优点

Secure:交易将由不同的节点执行多次 Decentralized

缺点

Expensive Slow

zk-rollup

ZK-rollup 是一种扩展解决方案,用于解决 EVM 的可扩展性问题.

ZK-rollup 不像 Layer1 广播所有交易,以及拥塞且昂贵的 P2P 网络,其有一个单独的 Layer2 网络层,可以更加中心化.

zk-Rollup 的基本思想是将大量交易聚合到一个 Rollup 块中,并为该链下的块生成 简洁,公开,可验证 的证明。然后 Layer 1 上的智能合约只需要验证证明并直接应用更新的状态,而无需重新执行那些交易。这可以帮助节省一个数量级的 gas 费用,以及提升一个数量级别的网络吞吐.因为验证证明比重新执行计算便宜得多。另一个节省来自数据压缩(即只保留最小的链上数据用于验证).

这样做与原来的安全性是**等效的.**背后的原理依赖于 zk.

编写困难

但是构造这样一个 Prover 是困难的,对于某些计算的证明,首先需要以电路形式编写所有程序逻辑,

也就是用加法乘法和类似的非常底层的方法断言.电路强调非常复杂的逻辑,包括 for Loop ,if else 和所有程序.语法非常复杂.此外 一个电路对应一个程序,这意味着对于不同的应用程序开发人员,需要实现自己的电路.电路也需要通过一个非常严格的安全测试审计,这需要很长的开发时间.

兼容 : 比如一个 Prover 无法同时包含来自 uniswap 与 optiswap 上的交易.

所以需要 zkevm.

zkevm 概念

zkEVM 是一种虚拟机,通过 zk 证明计算和现有以太坊基础设施兼容的方式执行智能合约交易。这使它们能够成为零知识汇总、第 2 层扩展解决方案的一部分,从而提高交易吞吐量,同时降低成本

如果第 2 层可以运行为以太坊环境创建的程序而无需修改底层智能合约逻辑,则该 Layer2 是 EVM 兼容的。这使得第 2 层与现有的以太坊智能合约模式、代币标准和工具兼容。与 EVM 兼容对于这些第 2 层的广泛采用非常重要,因为它使熟悉以太坊 Solidity 编程语言的开发人员能够使用他们习惯的的工具构建高度可扩展的应用程序。

但是 zkevm 很难编写,有以下几点原因

  • **第一,EVM 对椭圆曲线的支持有限。**目前,EVM 仅支持 BN254 配对。由于不直接支持循环椭圆曲线,因此很难进行递归证明。在此之下也很难使用其他专用协议。验证算法必须是 EVM 友好的。
  • **第二,EVM字节为 256 位。**EVM 在 256 位整数上运行(就像大多数正常 VM 在 32-64 位整数上运行),而 zk 证明“天生地”大多在素数上工作。在电路内部进行“不匹配的字段计算”需要范围证明,这将在每个 EVM 操作中增加约 100 个约束。这将使 EVM 电路大小扩大两个数量级。
  • **第三,EVM 有很多特殊的操作码。**EVM 与传统 VM 不同,它有许多特殊的操作码,例如 CALL。它也有与执行上下文和 gas 相关的错误类型。这给电路设计带来了新的挑战。
  • **第四,EVM 是基于堆栈的虚拟机。**SyncVM (zksync) 和 Cario (starkware) 的架构在基于寄存器的模型中定义了自己的中间表示(IR,Intermediate Representation)/代数中间表示(AIR, Algebraic Intermediate Representation)。他们构建了一个专门的编译器,将智能合约代码编译成一新的 zk 友好 IR。他们的方案是语言兼容而不是原生 EVM 兼容。基于堆栈的模型和直接支持原生链工具更难证明。
  • **第五,以太坊存储层带来巨大开销。**以太坊存储层高度依赖 Keccak 和巨大的 MPT,它们都不是 zk 友好的,并且需要巨大的证明开销。例如,Keccak 哈希比电路中的 Poseidon 哈希大 1000 倍。但是,如果将 Keccak 替换为另一个哈希算法,则会对现有的以太坊基础设施造成一些兼容性问题。
  • **第六,基于机器的证明需要巨大的开销。**即使能够妥善处理上述所有问题,仍然需要找到一种有效的方法将它们组合在一起以获得一个完整的 EVM 电路。正如我们上一节中所提到的,即使像 add 这样简单的操作码也需要整个 EVM 电路的开销

以下技术的发展使得 zkevm 得以落地

  • **多项式承诺的使用。**在过去的几年里,大多数简洁零知识证明协议都坚持使用 R1CS,将 PCP 查询编码在特定于应用程序的可信设置中。电路大小通常会爆炸,且不能进行许多自定义的优化,因为每个约束的项数需要为 2(双线性配对只允许指数中的一次乘法)。使用多项式承诺方案,可以通过通用设置甚至透明设置将约束提升到任何项数。这为后端的选择提供了极大的灵活性。
  • **查找表参数和自定义配置的出现。**另一个强大的优化来自查找表的使用。该优化首先在 Arya 中提出,然后 Plookup 中进一步升级。这可以为 zk 不友好的原语(即,AND、XOR 等按位运算)节省很多成本。自定义配置可以高效地进行高项数的约束。TurboPlonk 和 UltraPlonk 定义了优雅的程序语法,以便更轻松地使用查找表和定制配置。这对于减少 EVM 电路的开销非常有帮助。
  • **递归****证明越来越可行。**递归证明在过去需要巨大的开销,因为它依赖于特殊的配对友好的循环椭圆曲线,这引入了很大的计算开销。然而,更多的技术在不牺牲效率的情况下使这成为可能。例如,Halo 可以避免对配对友好曲线的需要,并使用特殊的内积参数来摊销递归成本。Aztec 表明可以直接对现有协议进行证明聚合(查找表可以减少非原生字段操作的开销,从而可以使验证电路更小)。它可以极大地提高支持的电路大小的可扩展性。
  • 硬件加速使证明更加高效。Scroll 为证明者制造了最快的 GPU 和 ASIC/FPGA 加速器。关于 ASIC 证明者的论文,今年已经被最大的计算机会议(ISCA)收录。GPU 证明器比 Filecoin 的实现快大约 5 到 10 倍。这可以大大提高证明者的计算效率。

ZKEVM 分类

  • Language level:采用高级语言(例如 Solidity 或 Vyper)编写的代码,并将其编译为旨在支持零知识证明的语言。本质上,它们相当于高级语言,但不是实际的 EVM。尽管合约可能不具有相同的地址,但这可以更快地生成证明并降低成本 Starknet
  • Bytecode level:牺牲了一些 EVM 功能,以实现更轻松的应用程序开发和证明生成,例如对预编译、VM 内存、堆栈以及智能合约代码处理方式的更改。虽然大多数以太坊应用程序都可以在这种环境中运行,但有些应用程序可能需要重写 Scroll Polygen
  • Consensus level:不会改变当前以太坊系统的任何部分,从而更容易生成零知识证明。这使得它们与所有以太坊本机应用程序完全兼容,并允许重复使用区块浏览器和执行客户端等工具。然而,以太坊协议的某些部分需要大量计算来生成零知识证明,导致 zkEVM 的证明时间较长

也可以参考 V 神的 4 种分类

Build a zkEVM from scratch

Interesting research problems

Other applications using zkEV

Lecture 16: Hardware Acceleration of ZKP

1.Goals of HW Acceleration

image-20230515111025421

  • 吞吐量,即每单位时间执行尽可能多的操作
  • 成本
    • 当优化成本时,目标是降低执行某些操作所涉及的资本和运营费用。对于比特币挖掘机来说,这意味着最大化每美元购买价值的哈希数量,同时最小化每个哈希的能源消耗,从而降低运营成本。
  • 延迟:减少完成单个操作的时间
    • 在高频交易等领域,延迟是一个重要的考虑因素。 低延迟的证明生成可以促进更好的用户体验或更快的确定用例,比如ZK Bridges。

2.What needs accelerated

image-20230515112018032

首先要注意的是每个证明系统及其相关实现都是利用不同的密码原语和不同的软件库构建的,在某一个证明系统中计算成本最高的部分,在另外不同的证明系统实现或用例中可能相对次要或可能根本不会出现,

其次不同的证明系统中,存在三种计算量大的操作,包括MSM,NTT,算术哈希。

3.MSM:多标量乘法

image-20230515121131312

MSM 是一种用于计算多个标量乘法之和的算法,或者它可以被认为是椭圆曲线点和标量的点积。

由于问题的性质,每个标量乘法或一组标量乘法都可以很容易地并行化,并且可以由不同的硬件引擎拆分和操作然后汇集并在最后累积,有许多优化可用于减少计算 MSM 的计算量,用于更大尺寸的 MSM 算法,如pipepenger。

image-20230515141320803pippengers,将计算成本从线性减少到O(n/logn),除了使用改进的算法之外, 还有替代的点表示方法(Jacobian)和曲线表示方法(Edwards)可用于减少每个曲线上的域元素的操作总数

image-20230515142135825通过将它们从像 CPU 这样的主机设备转移到更并行的架构,如 GPU,可以提高计算效率。然而,当将操作从主机设备移动到外围设备时,必须记住一件事情,即数据也必须被移动以进行计算。在多标量乘法的情况下,标量和点必须从主机移动到加速器上进行计算。这两个设备之间可用的通信带宽通常会限制加速器的最大性能。

4.NTT

image-20230515143228424

NTT 是一种用于将两个多项式相乘的算法 NTT 类似于其他算法,例如 fft 或 DFT,但它的独特之处在于它对有限域元素进行运算

实现 NTT 的常用算法之一是Cooley-Tukey算法,该算法将多项式乘法的复杂性从O(n^2)降低到O(nlog n)阶

image-20230515143651529 类似于 MSM 在主机设备上执行 NTT 时,标量也必须再次移至加速器 通信带宽将限制加速器的最大可能性能,但是 NTT不容易并行化。 每个元素必须在算法操作期间与各种其他元素交互,这意味着问题不能轻易进一步划分,因为这些元素与每个元素交互,它们必须保存在内存中并在强加高内存要求的情况下运行

5.算术hash

image-20230515144248585

许多零知识证明用例中的算术哈希它要求

证明哈希原像的知识或利用哈希 ,Merkle roots和 Merkel 包含路径有效地表示电路外部的数据。 算术哈希函数(如 Poseidon,rescue Prime)通常用于传统散列函数(如 ShA系列哈希函数)。

选择这些哈希函数是因为虽然本身它们的计算成本更高,但在电路内部使用时,部署效率会更高,因为这些哈希函数的constraints数量会更少。 在实例化哈希函数时可以选择许多算法参数,这可能会影响计算成本。其中一些参数包括有限域大小,有限域选择的素数大小,MDS 矩阵结构等。

算术哈希原语的有效实现主要由模乘法驱动,证明生成中涉及的计算量大的操作通常因系统而异

image-20230515145655014

这些操作取决于承诺方案,像KZG这样的承诺方案会导致在生成证明过程中 MSM 操作会主导。

而当使用FRI承诺方案时,证明生成过程通常由 NTT 主导。

许多 snark 系统,例如groth16 和 Marlin由 MSM 主导,而 Starks 总体上通常由 NTT 主导。

但是这三个先前讨论的密码原语(MSM,NTT,算术哈希)在加速之前在所有证明系统中占据了三分之二或更多的时间 这三个操作可能看起来截然不同,但它们实际上共享一些基础组件。

比如MSM and NTT的公共基础组件是域和曲线操作,这些操作的核心主要由域上的算术驱动,特别是模乘法

因此虽然这些算法的结构彼此,大不相同,但它们是基础的性能通常源自硬件执行模乘法的能力

image-20230515150923268

需要注意的一件有趣的事情是数据大小与模乘法计算成本之间的关系, 当数据大小呈线性增长,模乘法的计算成本相对于域的大小是N^2。

这意味着随着域大小的增长,加速器性能可能取决于操作的计算成本, 但对于较小的域大小,加速器可能会受到主机可用带宽的瓶颈

这种二分法凸显了在开始设计硬件加速系统时理解证明系统的具体参数的重要性。

它也凸显了设计能够服务于各种证明系统和参数的硬件加速设备或实现的难度。

6.提高证明生成性能

image-20230515153042757

改善证明生成性能的第一步是了解所使用的证明系统和用例的计算、内存和带宽成本,通过将高级操作(如 MSM 和 NTT)分解为计算它们所需的模乘数量。通常可以在完成实现之前估算证明系统在各种硬件平台上的性能。

然而,为了确保估计是准确的,有许多参数应该提前知道

第一个最重要的参数是证明系统中每个操作的数量, 例如一些证明 系统每个证明可能需要四个或更多 msms 而其他系统可能只需要两个

第二个关键因素是通常需要计算的操作的大小,不同的用例将导致每个操作的不同大小 例如在某些用例中 MSM将只有 1000 的大小,而在另一个用例中,它可能是 1000 万或更多

第三个因素是确定的是域和曲线的大小,这将有助于告知每个模块化算术运算的带宽和计算复杂性

此外,点的表述形式(Affine or Jacobian),模运算等等 最后还有各种其他较小的因素可能有助于证明系统的性能 一旦所有这些参数都确定了,执行证明或证明生成过程所需的模乘次数可以很容易地计算出来,有了这个数字,就可以 与给定硬件平台的模型性能进行比较,以便在了解需要执行的计算的情况下得出性能估计或计算时间

image-20230515155501729 硬件加速的下一步是为这些工作负载选择合适的硬件-主要由模乘法驱动

应该寻找可以快速且廉价地执行大量乘法的硬件平台

可通过查看平台上硬件乘法器以及每个乘法器可以执行的速度和频率来评估给定硬件平台的估计性能

image-20230515162532683

上图是一个包含四个硬件平台的表格:桌面CPU、服务器 CPU、FPGA 和 GPU

  • 第一个平台:桌面 CPU
    • 包含八个内核,每个内核都有一个 64 x 64 位乘法器,工作频率为 5GHz
    • 此平台的乘法功率估计约为164 该数字的计算方法是将乘法器的数量、乘法器大小和频率相乘,然后除以 1000
  • 第二个平台:服务器 CPU
    • 包含96 个内核,每个内核都有一个Multiplier,但以较低的频率运行,这个平台有大约 900 的Multi能力。是桌面处理器的五倍
  • 第三个平台:FPGA
    • 与服务器上存在的 96 个相比,超过 6000个乘法器。虽然乘法器数量大约是服务器CPU乘法器数量的 60 倍,但由于乘法器大小和频率的减少,乘法运算的功率小于服务器 CPU 的两倍。
  • 第四个平台:GPU
    • 大约5000个32*32位的乘法器,以 1.7 GHz 的频率运行,这产生了大约 9000乘法能力
    • 相较于FPGA,拥有更大的乘法器大小和更高的工作频率,性能得到提高

关于这些底层硬件架构及其对模块性能的影响,强烈推荐Simon puffer 几年前在斯坦福区块链会议上的演讲,它可以在 YouTube 上找到

这些分析仅突出硬件平台的基础功能,

为了实现提高性能并达到硬件加速的目标,通常还必须考虑其他因素。

包括实现理论性能的能力、部署的便利性、运营成本、esa编程和许多其他因素

成功的硬件加速需要关注的两个关键领域

image-20230515171544562

首先是选择适合目标平台的硬件友好算法

针对 GPU 和 FPGA 这样的目标平台具有数千个核心,最适合使用高度可并行化的算法。此外,在选择算法时,应选择旨在通过减少所需操作数量来降低总计算成本的算法。

一旦选择了算法,最后一步是创建高效的实现。通常情况下,需要重新构造算法以更好地匹配目标平台的硬件能力。除了重构算法外,通常还需要使用低级汇编原语来更充分地利用硬件资源并实现最大的性能。

7.硬件加速存在的限制与陷阱

image-20230515172632660

在追求硬件加速时,乘法不是唯一需要的资源。虽然这些高级原语主要由模数乘法组成,但算术单元中的其他计算资源通常也是必需的。此外,根据正在加速的操作的大小和类型,非计算资源也可能成为瓶颈。例如,像 NTT 这样的操作有时会受到内存访问速度的瓶颈限制。

另外,对于问题规模较大的用例,有时所需数据无法全部 在目标平台的内存中容纳,从而导致性能降低。对于连接到主机系统的加速器,通信带宽也可能成为瓶颈。目前,许多 GPU 和 FPGA 硬件加速的 NTT 实现受限于它们在主机和加速器之间传输数据的能力,而不是计算资源。有时可以通过将数据保留在加速器上来减少带宽需求,从而缓解或消除这些瓶颈。

image-20230515172813446

数据移动成为瓶颈而不是数据计算不仅在 NTT 和 ZKP 系统中出现,而且在大数据和高性能计算环境中普遍存在这种趋势。对于高度并行的算法,计算速度往往比数据移动本身更快,因此硬件加速设计应尽量减少数据移动。

在使用主机外加速器时,另一个需要考虑的因素是将数据移动到加速器和返回主机的时间。

对于小问题,有时在主机上直接进行计算可能比在加速器上更高效。

硬件加速的最后一个陷阱是广为人知的奥姆德尔定律或贝尔定律,它指出,通过优化系统的单个部分或单个部分获得的总体性能提高取决于改进部分实际使用的时间占总时间的比例。

更简单地说,在 ZKP 系统中,如果 MSM、NTT 和算术哈希占据大约 65% 的时间,即使这些操作被消除,最大的加速比也只能达到 3 倍。考虑到证明生成与本地计算的时间开销相差几十万到一百万倍,显然优化工作不会止步于此。

8.FileCoin的加速例子

image-20230515174119119

过去几年,Filecoin 一直是最大的 ZKP 系统之一,每天平均生成 1 到 5 百万个证明。

Filecoin 使用 ZKPS 来进行副本证明 (PRORAP),这是一种证明你已经创建了数据集的唯一副本的加密方式。Filecoin 中使用的副本证明需要大约 470 GB 的 Poseidon 哈希。

如果在许多核心的 CPU 系统上进行哈希运算,需要大约 100 分钟。

相比之下,Filecoin 的 GPU 实现仅需要大约一分钟,可以实现大约 100 倍的性能提升。

对于 Filecoin 中的密码学证明组件,他们利用了 Groth16 协议。在 Filecoin 网络上进行每个 PoRep 时,存储提供者会生成 10 个证明,每个证明大约有 1.3 亿个约束条件,总共超过 10 亿个约束条件。仅用于创建这些证明的 MSM 就总计约为 45 亿个点标量对

如果这些证明在许多核心的 CPU 上计算,需要约一个小时才能完成。相比之下,在 GPU 上可以在大约三分钟内完成,这是一个大约 20 倍的性能提升。这个例子突显了硬件加速让ZKP 用例变得实际可行的能力。

9.zk加速的现在与未来

image-20230515174833294

了解更多关于硬件加速的知识,有许多在线资源可用,包括许多今天讨论的加密原语的开源 GPU 和 FPGA 实现。

一个特别好的资源是 zprize.io,这是一个旨在改善 ZKP 系统性能的社区倡议。

对用于更大的多标量乘法,单个 GPU 可以以每秒超过 1 亿个Bases的速度执行, 就 NTT 而言,大小为 2 到28 的 NTT 可以在 250 毫秒内计算出来, 对于 Poseidon 哈希,GPU 可以大约哈希 每秒 350 GB。

image-20230515175249404

尽管在过去几年中,ZKP 硬件加速取得了巨大进展,但仍有很大的改进空间。下面是一些可以帮助证明生成更快的领域。

第一个领域是针对核心原语(如 MSM 和 NTT)的改进算法或对现有算法的其他优化。

第二个领域是全新的核心原语,如具有更低计算要求的新哈希函数。

第三个领域即新的证明系统,特别是关于硬件加速的简化证明系统。简化的证明系统可以为硬件加速创造更多机会。

例如,更简化的证明系统可以减少不同操作、减少通信和内存要求,甚至消除一些目前存在的计算昂贵的操作。最后,改进实现的空间也永远存在,包括完整的证明系统和硬件加速的原语。这包括针对商用 GPU 和 FPGA 等现成硬件以及定制硅片(例如 ASIC)的设计。

Reference

Amber Group.“Need for Speed: Zero Knowledge.Introduction I by Amber Group

Feng, Boyuan. "Multi-scalar Multiplication (MSM) .

Figment Capital.“Accelerating Zero-Knowledge Proofs.

Jane Street. "Accelerating zk-SNARKs - MSM and NTT alorithms on FPGAs with Hardcaml." Jane Street Tech Blog, 7 December 2022.

Thaler, Justin. "Measuring SNARK performance: Frontends, backends, and the future." a16z crypto, 11 August 2022

Zhang, Ye. "ZKP MOOC Lecture 12: zkEVM Design, Optimization and Applications.

[[# Arithmetic circuits

2.1 Encoding the trace as arithmatic constraints

R1CS

  • **Flattening:将电路的执行转换成计算轨迹,**即将复合函数以乘法为基本单元拆解成一组有序的简单函数,其中
    • , 为输出变量,为左输入变量,为右输入变量
    • 这里的有序是指按电路执行的顺序
    • 这里会引入中间变量
      • 除了根节点处的门之外,其它的门的输出引脚添加对应的中间变量
      • 除了叶子节点处的门之外,其他的门的输入引脚添加中间变量,该中间变量来自于另一个门的输出
      • 举例说明:若门A的输出引脚接入到门B的输入引脚,则为门A的输出引脚和门B的输入引脚添加同一个中间变量
  • 重组中的数据:将其变成一阶约束系统R1CS:(注: 为Hadamard product,按位乘法):
    • ,即由表示1的冗余变量,函数输出,输入变量,中间变量构成的集合对应的向量。
    • 的输出变量基于的选择向量构成的矩阵
    • 的左输入变量基于的选择向量构成的矩阵
    • 的右输入变量基于的选择向量构成的矩阵
  • 注:矩阵的行数等于乘法门的数量,矩阵的列数等于中元素的数量,即变量的数量

Plonkish Arithmetization

  • **Flattening:**即将复合函数拆解成一组离散的门,其中
    • 为输出变量,为左输入变量,为右输入变量,为常量,为输出选择器,为左输入变量选择器,为右输入变量选择器,为乘积选择器,为常数选择器
    • **注:**矩阵的行数等于所有门的数量,即约束的数量,n。
    • 这里的有序是指计算的顺序
    • 这里会引入中间变量
      • 除了根节点处的门之外,其它的门的输出引脚添加对应的中间变量
      • 除了叶子节点处的门之外,其他的门的输入引脚添加中间变量,该中间变量来自于另一个门的输出
      • 举例说明:若门A的输出引脚接入到门B的输入引脚,则为门A的输出引脚和门B的输入引脚添加同一个中间变量
  • **重组中的数据:**将其变成:(注: 为Hadamard product,按位乘法)
    • ,即选择器矩阵
    • ,即变量矩阵
    • 轮换置换后得到的位置集合,来自于Wiring
  • Wiring(Copy Constraints)
    • 分析:Wiring即将离散的门连接起来,即某一个门的输出引脚要接入另一个门的输入引脚约束变量矩阵中某几个位置的元素是相等的这一个元素出现在矩阵的多个位置处

    • Wiring实现思路:

      • 矩阵中的每一个位置从1到3n进行唯一编号,则所有的编号构成一个位置集合,将位置集合对应的元素取出构成一个multiset
      • 把每个元素出现在中的位置编号取出放在一个集合中,即一个元素对应一个位置集合。将位置集合对应的元素取出构成一个multiset 。所有元素的的并集即为的并集为
      • ,要使得 中元素全部相等

      与其进行轮换置换后得到的集合在Multiset的意义上是等价的

      在Multiset的意义上是等价的

      为所有的并集,为所有的并集,为所有的并集, 在Multiset的意义上是等价的

      ,取随机数,有

      • 至此,问题转化成如何证明连乘等式 ,即证明一个n步递归,

        • 初始值:
        • 递归定义:
        • 终止条件:

        则有

        所有 构成向量

        • 至此,Wiring转化成三个约束
          • 即约束向量的指定位的值为k,即约束的第1位()的值为1,第n+1位()的值为

            为n维向量空间的标准基的第i个基向量,向量的第位为等价于:

2.2 Constraints Merge

R1CS to QAP

  • 带有Hadamard product运算的n维向量的群,和带有乘法运算的在上的最高项次数不大于n-1的单变量多项式的群,映射:,令,有,是群同态

  • , 至此,完成了从R1CS到QAP到转换

Plonkish Arithmetization to QAP

Plonkish Arithmetization包含两部分约束:

第一部分约束每个门是正确计算的,即所谓算术约束;第二部分约束门与门之间正确连接,即所谓复制约束。

首先来转换算术约束:

  • 带有加法的n维向量的群,和带有加法的在上的最高项次数不大于n-1的单变量多项式的群,映射:,令,有$h(\vec m)= \langle \vec m,\vec L(X) \rangleLemma5:由Lemma1,Lemma4,Lemma5,有\vec{q_O}\circ \vec{w}=\vec{q_L}\circ\vec{u}+\vec{q_R}\circ\vec{v}+\vec{q_M}\circ(\vec{u}\circ\vec{v})+\vec{q_C}\circ\vec{c} \\ \iff \langle \vec q_O,\vec L(X) \rangle \cdot \langle \vec w,\vec L(X) \rangle=\langle \vec q_L,\vec L(X) \rangle \cdot \langle \vec u,\vec L(X) \rangle+\langle \vec q_R,\vec L(X) \rangle \cdot \langle \vec v,\vec L(X) \rangle+\langle \vec q_m,\vec L(X) \rangle \cdot (\langle \vec u,\vec L(X) \rangle\cdot\langle \vec v,\vec L(X) \rangle )+\langle \vec q_C,\vec L(X) \rangle a(X) =\langle \vec a,\vec L(X) \rangleq_O(X)w(X)=q_L(X)u(X)+q_R(X)v(X)+q_M(X)u(X)v(X)+q_C(X)Lemma1\vec e_i \circ \vec r=k\times \vec e_i \\ \iff L_i(X)r(X)=k\times L_i(X) \\ \iff L_i(X)(r(X)-k)=0r_0=1 \iff L_0(X)(r(X)-1)=0 \\ r_n=c \iff L_n(X)(r(X)-c)=0 L_i(X)HLagrange BasisLemma1\vec r_{i}=\vec r_{i-1} \circ \vec b_{i-1} \\ \iff \langle \vec r,\vec L(\omega \cdot X) \rangle =\langle \vec r,\vec L(X) \rangle \cdot \langle \vec b,\vec L(X) \rangle

  • 至此,复制约束转换成了三个多项式约束。

2.3 A function commitment scheme

在2.3中得到了一系列多项式之间的约束,本节我们来看如何实现多项式约束,

令:

\mathcal F:=function\ family\mathbb F_p:= 有限域

对于\mathcal Fsetup(\lambda) \to ppcommit(pp,f,r) \to com_f基于随机数r对f\in \mathcal F的承诺eval(prover \ P,verifier\ V)com_fx\in X,y\in Y:证明f(x)=y,即所谓的将f在点(x,y)处打开P(pp,f,x,y,r) \to 简短证明\piV(pp,com_f,x,y,\pi)\to 接受/拒绝

三类典型的Function Family Commiments

  • Polynominal commitments:次数不大于d的单变量多项式承诺 f(X)\in \mathbb F_p^{(\leq d)} [X]f(X_1,...,X_k)\in \mathbb F_p^{(\leq 1)}[X_1,...,X_K]f_{\vec v}(\vec u)= \left \langle \vec u, \vec v \right \rangle=\sum _{i=1}^nu_iv_i

这三者从上到下,越来越general

PCS: Polynominal Commitment Scheme

适用于次数不大于d的单变量多项式 f(X)\in \mathbb F_p^{(\leq d)} [X]

Some usual PSC

  • Bulletproofs:基于椭圆曲线,verifier的算法复杂度与d成线性相关
  • KZG‘10,Dory’20:基于双线性椭圆曲线
  • Dark’20:基于阶未知的群
  • FRI:基于hash Function

KZG poly-commit scheme

  • 预备知识:阶为p的群\ \mathbb G:=\{1,G,2\cdot G,3\cdot G,...,(p-1)\cdot G\} ,其中,G为生成元setup(\lambda) \to pp\alpha\in \mathbb F_ppp=(H_0=1,H_1=\alpha \cdot G,H_2=\alpha^2 \cdot G,...,H_d=\alpha^d \cdot G)\in \mathbb G^{d+1}\alpha\alpha\alphacommit(pp,f,r) \to com_fcom_f:=f(\alpha)\cdot G \in \mathbb Gf(X)=f_0+f_1X+...+f_dX^d\implies com_f=f_0\cdot 1+f_1\cdot\alpha G+f_2\cdot\alpha^2 G+ ...+f_d\cdot \alpha^dG\iff com_f=f_0\cdot H——1+f_1\cdot H1+...+f_d\cdot H_deval(prover \ P,verifier\ V)f(u)=vf(u)=v\iff u是 \hat f =f-v的根\iff (X-u)整除\hat f\iff \exists q\in \mathbb{F}_p[X]\ \ s.t.\ \ q(X)\cdot(X-u)=f(X)-vProver(pp,f,u,v)商多项式q(X) 及其承诺com_qVerifier(pp,com_f,u,v)(\alpha-u)\cdot com_q=com_f-v\cdot G\alpha\alpha(\alpha-u)\cdot com_q=com_f-v\cdot Gcom_q和com_f\alpha(\alpha-u)setup(\lambda) \to pp\alpha\in \mathbb F_ppp=(H_0=1,H_1=\alpha \cdot G,H_2=\alpha^2 \cdot G,...,H_d=\alpha^d \cdot G)\in \mathbb G^{d+1} + (T_0=1,T_1=\alpha \cdot G_2) \in \mathbb G_2^1\alpha\alpha\alphaVerifier(pp,com_f,u,v)e\in \mathbb G \times \mathbb G_2 \to \mathbb G_X$ - 至此,将原来需要验证的,转换成了 在上验证

2.4 Polynominal IOP

Useful Lemma

  • Lemma1: Schwartz zipple定理

  • Lemma2: 单位根和乘法子群**:**

    为k次单位根,即

    乘法子群

    由于单位根的对称性,有

  • Lemma3: 中的元素均为的根,即

    存在商多项式

Poly-IOP可以高效完成的任务

  • Task1 zero-test:证明在H上等于0,即证明H中的元素均为的根
  • Task2 sum-check:证明,即证明在H上全部取值的和等于b
  • Task3 prod-check:证明,即证明在H上全部取值的和等于c

Zero Test on H

  1. Prover 向 Verifier Commit
  2. Verifier 向Prover 发送随机数r
  3. Verifier 检查

参考资料

https://github.com/sec-bit/learning-zkp/blob/develop/plonk-intro-cn/plonk-arithmetization.md

https://www.youtube.com/watch?v=J4pVTamUBvU&list=PLj80z0cJm8QErn3akRcqvxUsyXWC81OGq&index=2

https://github.com/sec-bit/learning-zkp/blob/develop/plonk-intro-cn/plonk-polycom.md ](https://github.com/zkp-co-learning/ZKP/edit/main/%E7%AC%AC%E4%BA%8C%E7%AB%A0.md)https://github.com/zkp-co-learning/ZKP/edit/main/%E7%AC%AC%E4%BA%8C%E7%AB%A0.md](https://github.com/zkp-co-learning/ZKP/edit/main/%E7%AC%AC%E4%BA%8C%E7%AB%A0.md)https://github.com/zkp-co-learning/ZKP/edit/main/%E7%AC%AC%E4%BA%8C%E7%AB%A0.md

本文假设您对椭圆曲线运算及哈希函数等有着基础的了解

简洁的 Schnorr 协议

Alice 拥有一个秘密数字,a,我们可以把这个数字想象成「私钥」,然后把它「映射」到椭圆曲线群上的一个点 a*G,简写为 aG。这个点我们把它当做「公钥」。

  • sk = a ( secret key = a )
  • PK = aG

a secret key  that corresponds to a public key .

请注意「映射」这个词,给任意一个有限域上的整数 r,我们就可以在循环群中找到一个对应的点  rG,或者用一个标量乘法来表示 r*G。但是反过来计算是很「困难」的,这是一个「密码学难题」—— 被称为离散对数难题。

取模之后 , 就很难知道原来的指数是多少了。 事实上,如果模取得相当大,从运算结果倒推指数运算就不可行了;现代密码学很大程度上就是基于这个问题的“困难”

也就是说,如果任意给一个椭圆曲线循环群上的点 R,那么到底是有限域中的哪一个整数对应 R,这个计算是很难的,如果有限域足够大,比如说 256bit 这么大,我们姑且可以认为这个反向计算是不可能做到的

Schnorr 协议充分利用了有限域和循环群之间单向映射,实现了最简单的零知识证明安全协议:Alice 向 Bob 证明她拥有 PK 对应的私钥 sk

  1. 第一步:为了保证零知识,Alice 需要先产生一个随机数 r,这个随机数的用途是用来保护私钥 无法被 Bob 抽取出来。这个随机数也需要映射到椭圆曲线群上即 rG。 ( 映射之后 , Bob 就不可能通过 rG 推算出 r )
  2. 第二步:Bob 要提供一个随机数进行挑战,我们把它称为  c
  3. 第三步:Alice 根据挑战数 c 计算  z = r + c * a (即sk),把 z 发给 Bob,Bob 在自己这边通过下式进行检验:
#![allow(unused)]
fn main() {
z*G ?= R + c*PK 
    ?= rG + c*(aG)
}

大家可以看到 Bob 在第三步「同态地」检验 z 的计算过程。如果这个式子成立,那么就能证明 Alice 确实有私钥 a。可是,这是为什么呢?

z 的计算和验证过程很有趣,有几个关键技巧:

  1. 首先 Bob 必须给出一个「随机」挑战数 ,然后 Bob 在椭圆曲线上同态地检查 z 。如果我们把挑战数    看成是一个未知数,那么 r+a*c=z 可以看成是一个一元一次方程,其中 r 与 a 是方程系数。请注意在 c 未知的前提下,如果 r + a*x = r' + a'*x 要成立,那么根据 Schwatz-Zippel 定理,极大概率上 r=r'a=a' 都成立。也就是说, Alice 在 c 未知的前提下,想找到另一对不同的 r',a' 来计算 z 骗过 Bob 是几乎不可能的。这个随机挑战数 c 实现了r 和 a 的限制。虽然 Bob 随机选了一个数,但是由于 Alice 事先不知道,所以 Alice 不得不使用私钥 a 来计算 z。这里的关键: c 必须是个随机数。
  2. Bob 验证是在椭圆曲线群上完成。Bob 不知道 r ,但是他知道 r  映射到曲线上的点 R ;Bob 也不知道 a,但是他知道 a 映射到曲线群上的点 PK,即 a*G。通过同态映射与Schwatz-Zippel 定理,Bob 可以校验 z 的计算过程是否正确,从而知道 Alice 确实是通过 r 和 a 计算得出的 z,但是又不暴露 r 与 a 的值。
  3. 还有,在协议第一步中产生的随机数 r 保证了 a 的保密性。因为任何一个秘密当和一个符合「一致性分布」的随机数相加之后的和仍然符合「一致性分布」。

看懂了这个图就看懂了 !!!!!

是 Sigma 零知识证明的一个特例

Schnorr 的非交互式版本

Schnorr 协议的非交互式版本可以避免 Prover 与 Verifier 的交互,但这要求 Prover 使用哈希函数,这样他就无法预测哈希函数的输出,非交互式版本的验证器实现非常简单,因为它不需要随机数生成器

(Making the protocol non-interactive)

首先定义: 即私钥 ; 是 Public key 即公钥 ;

  1. Prover 生成一个随机数 并创建一个承诺 , Prover 对 进行哈希处理以获得挑战值 ,
  2. Prover 创建对挑战的响应 , 然后将元组 (comm, s) 发送给验证者。

Verifier 自己计算 , 然后验证 :

如果 Verifier 自己验证这个等式相等, 则 Prover 就通过 这种方式隐藏了私钥 , 同时又能让对方确信自己真的有这个私钥 .

  1. The prover generates a random number r and creates a commitment com = gʳ. The prover hashes  gcom and y to get challenge cc = Hash(g, y, t).
  2. The prover creates a response to the challenge as s = r + c*x. The prover sends tuple (t, s) to the verifier.

The verifier now generates the same challenge c as Hash(g, y, t) and again checks if  equals yᶜ.tPython code demonstrating this protocol.

Schnorr 的问题

对不同的消息, 如果不幸选了相同的随机数 私钥就会泄露

如果 Alice 在两次交互过程中使用了同一个 K,那么 Bob 可以通过发送两个不同的 c 和 c' 来得到 s 和 s',然后通过下面的公式算出私钥 a

s  = (c +a*e)/k , 
s' = (c'+a*e)/k , 两式相减, 求出 k 

k = (c - c')/(s - s')
a = (k * s - c)/e

ECDSA

Bitcoin 和 ETH 都支持 ECDSA signature.

why need ECDSA?

除了显而易见的“我需要对一份文件/合同进行签名”,还有一个非常流行的应用场景:让我们以一个不想自己的数据被用户修改或者破坏的应用程序为例,比如一个只允许你载入官方地图和不可修改的模块的游戏,或者一部只允许你安装官方应用程序的手机或其它设备。

在这些案例当中,相关文件(应用程序、游戏地图、数据等)会用 ECDSA 进行签名,公钥会随应用程序/游戏/设备一起捆绑并用来验证签名来确保数据没有被修改,而私钥在本地一个私密的地方进行保存。由于你可以用公钥对签名进行验证,但是不能用它创建或者伪造新的签名,你可以无所顾忌地将公钥随应用程序/游戏/设备一起分发。

这与AES相比,区别是显而易见的。AES加密系统允许你对数据进行加密,但是你需要用密钥来解密,这就要求你将密钥与应用程序一起捆绑,破坏了对数据进行保护防止数据被用户修改的目的。

一个很好的例子就是PS3的控制台,它被大量的破解,所有的文件可以解密,所有的密钥可以从解密的文件当中抽取,但是为了能够在最新的固件上面运行程序,你还需要破解一个ECDSA的数字签名。

当你想要对一个文件进行签名的时候,你会用这个私钥 / 随机数 / 文件的哈希组成一个魔法数学方程,这将给出你的签名。签名本身将被分成两部分,称为 RS

  • 选择随机数 , 计算承诺 :
  • 挑战 : 取 的横坐标为 (先 mod , 再 mod )
  • 响应 :

为了验证签名的正确性,你只需要公钥(用私钥在曲线上面产生的点)并将公钥和签名的一部分 S 一起代入另外一个方程,如果这个签名是由私钥正确签名过的数字签名,那么它将给出签名的另外一部分 R

简单来说,一个数字签名包含两个数字,RS,然后你使用一个私钥来产生 RS ,如果将公钥和 S 代入被选定的魔法数学方程给出 , 且 的话,这个签名就是有效的。仅仅知道公钥是无法知道私钥或者创建出数字签名。

Algorithm

初始化: 椭圆曲线生成元为 ,标量域为 ,基域为

基域 理解为椭圆曲线点的横纵坐标的取值范围 标量域 即做倍点运算的标量的取值范围, 比如 里的 , 其不会超过椭圆曲线的阶

密钥生成: 私钥 和公钥

签名: 输入任意消息 , 计算

  • 选择随机数 , 计算承诺 :
  • 挑战 : 取 的横坐标为 (先 mod , 再 mod )
  • 响应 : ( k 增加了 ECDSA 的难度)

则签名为

的乘法逆元

我们是如何对一个文件或者一个信息进行签名的呢?

  1. 你需要知道签名本身是 40 字节,由各20字节的两个值来进行表示,第一个值叫作 ,第二个叫作
  2. 值对 放到一起就是你的 ECDSA 签名

验证 :

验证它,也非常的简单,你只需要 [公钥] 和导出这个公钥的曲线参数就可以了。你用以下方程来计算

Verifier :

  1. 输入消息 , 计算
  2. 校验
  3. 计算

的横坐标为 , 校验等式 : 如果相等, 则接受 , 否则拒绝

公式推导过程如下:

这里知道 还是可以推算私钥, 所以 EIP-32 要求 :

EdDSA

以太坊 BN256 曲线已经支持了 EdDSA

EdDSA 正是为了解决 Schnorr 签名私钥泄露的问题 : 他不是选择随机数, 而是计算随机数

初始化 : 椭圆曲线生成元为 , 阶为 密钥生成 : 私钥为 , 公钥为

签名: 消息为 ,计算随机数 **,计算承诺

计算挑战

计算响应

签名为(R,s)

验证: 重新计算挑战 ,然后校验

与 ECDSA 最大的区别在于 是算出来的, 没有使用随机数 这样产生的签名结果是确定性的,即对同一消息, 签名结果相同, 不会额外泄露信息

一般说来随机数是安全措施中重要的一种方法,但是随机数的产生也是安全隐患,著名的索尼公司产品 PS3 密钥泄露事件,就是随机数产生的问题导致的 (写死在了代码里, 晕)。

zk-SNARK

在聊 zk-SNARKs 之前, 首先来看 NARK(Non-interactive ARgument of Knowledge) :

  • C : 电路 Circuit
  • : 公开声明 public statement
  • : witness
  • 预处理(Preprocessing) 也称为 Setup, 它以电路的描述作为输入,然后输出这些公开参数,我们称之为 :
  • 表示公开的参数,供证明者使用。
  • 表示公开的参数,供验证者使用。

证明者和验证者各自会输入 :

  • prover takes the (public statement) & (public (circuit)params) & the Witness
  • Verifier takes & (public statement)

然后,证明者试图向验证者证明: It knows some such that

NARK Definition : A pre-processing NARK is a triple , where :

  • generate the Circuit's as public params for P & V.
  • : proof
  • : or

所有算法和对手都可以访问 随机预言机 (random oracle)

zk-SNARKs 条件是苛刻的, 因为要让 Verifier 在如此短的时间内完成某些验证, 我们需要一些新的方法来去处理计算, 比如多项式承诺 (polynomial commitment)

(To be continued ...)

Reference :

Vitalik ZK_Snark zk-learning Lectures 安比 zk-snarks https://vitalik.ca/general/2021/01/26/snarks.html Zero Knowledge Proofs with Sigma Protocols

从代码中学习 Plonk 协议

写作本文的目的主要是希望从代码的角度理解 Plonk 协议。因为我是开发者,之前读文章遇到公式感觉比较抽象,所以希望有这样的文章,可以从代码的角度来阐述 ZKP 的协议是如何工作的。

这篇文章对应的源代码在这里,主要实现了 Plonk 协议的核心概念,需要结合郭宇老师的 Plonk 系列文章阅读。

流程

通过测试 test.py 看到验证 Plonk 协议主要分为以下几个部分:

  • Setup
  • Program
  • Assignment
  • Generate proof
  • Verify
def prover_test():
    print("Beginning prover test")
    # powers should be 2^n so that we can use roots of unity for FFT
    # and should be bigger than len(coeffs) of polynomial to do KZG commitment
    # the value here is: powers = 4 * group_order
    # which is bigger than the order of quotient polynomial
    group_order = 8
    powers = group_order * 4
    setup = Setup.generate_srs(powers)

    program = Program(["e public", "c <== a * b", "e <== c * d"], group_order)
    assignments = {"a": 3, "b": 4, "c": 12, "d": 5, "e": 60}
    prover = Prover(setup, program)
    proof = prover.prove(assignments)
    print("Prover test success")
    return setup, proof, group_order

def verifier_test(setup, proof, group_order):
    print("Beginning verifier test")
    program = Program(["e public", "c <== a * b", "e <== c * d"], group_order)
    public = [60]
    vk = setup.verification_key(program.common_preprocessed_input())
    assert vk.verify_proof(group_order, proof, public)
    print("Verifier test success")

整个协议的过程:

  1. 给定一个计算/电路/程序:
a * b = c
c * d = e
其中 e 是公开值
  1. prover 选择特定的一组值 witness = (a, b, c, d, e),这组值满足上面的约束条件
  2. 在保持 witness 不公开的前提下,prover 生成一个证明 proof, 可以证明 prover 知道 witness
  3. verifier 验证 proof 的真实性

下面我们通过代码依次看看每个步骤都做了什么。

Setup

@dataclass
class Setup(object):
    #   ([1]₁, [x]₁, ..., [x^{d-1}]₁)
    # = ( G,    xG,  ...,  x^{d-1}G ), where G is a generator of G_1
    powers_of_x: list[G1Point]
    # [x]₂ = xH, where H is a generator of G_2
    X2: G2Point

    @classmethod
    def generate_srs(cls, powers: int):
        print("Start to generate structured reference string")
        # tau is a random number whatever you choose
        tau = 218313819403157342856071133

        # Initialize powers_of_x with 0 values
        powers_of_x = [0] * powers
        # powers_of_x[0] =  b.G1 * tau**0 = b.G1
        # powers_of_x[1] =  b.G1 * tau**1 = powers_of_x[0] * tau
        # powers_of_x[2] =  b.G1 * tau**2 = powers_of_x[1] * tau
        # ...
        # powers_of_x[i] =  b.G1 * tau**i = powers_of_x[i - 1] * tau
        powers_of_x[0] = b.G1

        for i in range(powers):
            if i > 0:
                powers_of_x[i] = b.multiply(powers_of_x[i - 1], tau)

        assert b.is_on_curve(powers_of_x[1], b.b)
        print("Generated G1 side, X^1 point: {}".format(powers_of_x[1]))

        X2 = b.multiply(b.G2, tau)
        assert b.is_on_curve(X2, b.b2)
        print("Generated G2 side, X^1 point: {}".format(X2))

        assert b.pairing(b.G2, powers_of_x[1]) == b.pairing(X2, b.G1)
        print("X^1 points checked consistent")
        print("Finished to generate structured reference string")

        return cls(powers_of_x, X2)

    # Encodes the KZG commitment that evaluates to the given values in the group
    def commit(self, values: Polynomial) -> G1Point:
        if (values.basis == Basis.LAGRANGE):
            # inverse FFT from Lagrange basis to monomial basis
            coeffs = values.ifft().values
        elif (values.basis == Basis.MONOMIAL):
            coeffs = values.values
        if len(coeffs) > len(self.powers_of_x):
            raise Exception("Not enough powers in setup")
        return ec_lincomb([(s, x) for s, x in zip(self.powers_of_x, coeffs)])

    # Generate the verification key for this program with the given setup
    def verification_key(self, pk: CommonPreprocessedInput) -> VerificationKey:
        return VerificationKey(
            pk.group_order,
            self.commit(pk.QM),
            self.commit(pk.QL),
            self.commit(pk.QR),
            self.commit(pk.QO),
            self.commit(pk.QC),
            self.commit(pk.S1),
            self.commit(pk.S2),
            self.commit(pk.S3),
            self.X2,
            Scalar.root_of_unity(pk.group_order),
        )
    

这里有几个函数,第一个函数 generate_srs 用于生成 structured reference string(SRS),用于给多项式在群上生成 KZG commitment。基本流程:

  1. 选择一个随机数作为 tau 的值
  2. 从椭圆曲线上获得两个生成元 G1 和 G2,有现成的函数库可以拿到
  3. 生成所需要的 SRS 值
  4. 最后对生成的值进行验证

第二个函数 commit,就是实际用来生成 KZG commitment 的函数

第三个函数 verification_key 用来给 verifier 生成 verification key,用来验证 proof

Program

program = Program(["e public", "c <== a * b", "e <== c * d"], group_order)

Program 类的目标是 Arithmetization,将某种计算转换成数学表示。这里的计算指的是一段电路,数学表示指的是多项式。

在 Plonk 中,可以用八个多项式来表示这个 Program: QL, QR, QM, QO, QC, S1, S2, S3。所以 Program 类的主要目标就是处理上面电路的字符串的表示,最终得到这八个多项式。这八个多项式是公开的,prover 和 verifier 都可以得到这个信息。

经过一定的处理,prover 得到 prover key(pk)。

@dataclass
class CommonPreprocessedInput:
    """Common preprocessed input"""

    group_order: int
    # q_M(X) multiplication selector polynomial
    QM: Polynomial
    # q_L(X) left selector polynomial
    QL: Polynomial
    # q_R(X) right selector polynomial
    QR: Polynomial
    # q_O(X) output selector polynomial
    QO: Polynomial
    # q_C(X) constants selector polynomial
    QC: Polynomial
    # S_σ1(X) first permutation polynomial S_σ1(X)
    S1: Polynomial
    # S_σ2(X) second permutation polynomial S_σ2(X)
    S2: Polynomial
    # S_σ3(X) third permutation polynomial S_σ3(X)
    S3: Polynomial

verifier 得到 verification key,在上面的 Setup 步骤中也提到了。

    # Generate the verification key for this program with the given setup
    def verification_key(self, pk: CommonPreprocessedInput) -> VerificationKey:
        return VerificationKey(
            pk.group_order,
            self.commit(pk.QM),
            self.commit(pk.QL),
            self.commit(pk.QR),
            self.commit(pk.QO),
            self.commit(pk.QC),
            self.commit(pk.S1),
            self.commit(pk.S2),
            self.commit(pk.S3),
            self.X2,
            Scalar.root_of_unity(pk.group_order),
        )

和 prover 不一样的是,为什么中间 8 个值要用 commitment 的形式发给 verifier 呢?这是因为 Plonk 协议为了保证 verifier 端验证的计算复杂度尽量低,所以没有给出原始的多项式,而只给出了 KZG 承诺的值,后面会看到,verifier 通过 pairing 验证就可以保证这些承诺值和原始的多项式是一一对应的,prover 欺骗不了 verifier,这样既保证的正确性,也保证了 verifier 验证的简单性。

Assignment

assignments = {"a": 3, "b": 4, "c": 12, "d": 5, "e": 60}

Assignment 是对电路中引线的赋值,也叫 witness 或者 private input。这些值只有 prover 知道,对 verifier 是保密的。prover 最终要向 verifier 提供证明,保证将这些值输入到 program 中能得到指定的结果。

Generate proof

proof = prover.prove(assignments)

这里是协议的重点,分为五轮。

主体逻辑:

    def prove(self, witness: dict[Optional[str], int]) -> Proof:
        # Initialise Fiat-Shamir transcript
        transcript = Transcript(b"plonk")

        # Collect fixed and public information
        # FIXME: Hash pk and PI into transcript
        public_vars = self.program.get_public_assignments()
        PI = Polynomial(
            [Scalar(-witness[v]) for v in public_vars]
            + [Scalar(0) for _ in range(self.group_order - len(public_vars))],
            Basis.LAGRANGE,
        )
        self.PI = PI

        # Round 1
        msg_1 = self.round_1(witness)
        self.beta, self.gamma = transcript.round_1(msg_1)

        # Round 2
        msg_2 = self.round_2()
        self.alpha, self.fft_cofactor = transcript.round_2(msg_2)

        # Round 3
        msg_3 = self.round_3()
        self.zeta = transcript.round_3(msg_3)

        # Round 4
        msg_4 = self.round_4()
        self.v = transcript.round_4(msg_4)

        # Round 5
        msg_5 = self.round_5()

        return Proof(msg_1, msg_2, msg_3, msg_4, msg_5)

通过 5 轮的计算会生成必要的 proof,这些 proof 之后交给 verifier 进行验证,如果通过,则整个协议完成。

Round 0: 初始化:

    def __init__(self, setup: Setup, program: Program):
        self.group_order = program.group_order
        self.setup = setup
        self.program = program
        self.pk = program.common_preprocessed_input()

Round 1: 生成对 witness/assignments 多项式的承诺

这个过程和相关知识可以参考 理解 PLONK(一):Plonkish Arithmetization 理解 PLONK(二):多项式编码 。多项式承诺相关知识可以参考 理解 Plonk(五):多项式承诺

大体流程:

  1. 根据 group_order 初始化 A, B, C 这三个 witness 多项式的点值 A_values,B_values,C_values,这些点值用于后面生成多项式,也就是生成的 Polynomial
  2. 依次读取 program 中引线的值,将左引线的值 L 添加到 A_values, 右引线的值添加到 B_values 中,输出引线的值添加到 C_values 中
  3. 通过 Polynomial 类生成 A,B,C 多项式
  4. 生成 A,B,C 多项式的 KZG 承诺
  5. 验证门约束等式是否成立
    def round_1(
        self,
        witness: dict[Optional[str], int],
    ) -> Message1:
        program = self.program
        setup = self.setup
        group_order = self.group_order

        if None not in witness:
            witness[None] = 0

        # 1. 根据 group_order 初始化 A, B, C 多项式的点值
        # A_values,B_values,C_values,这些点值用于后面
        # 生成多项式,也就是 `Polynomial` 类
        # Compute wire assignments
        A_values = [Scalar(0) for _ in range(group_order)]
        B_values = [Scalar(0) for _ in range(group_order)]
        C_values = [Scalar(0) for _ in range(group_order)]

        # 2. 依次读取 program 中引线的值,将左引线的值 L 添加到 A_values, 
        # 右引线的值添加到 B_values 中,输出引线的值添加到 C_values 中
        for i, gate_wires in enumerate(program.wires()):
            A_values[i] = Scalar(witness[gate_wires.L])
            B_values[i] = Scalar(witness[gate_wires.R])
            C_values[i] = Scalar(witness[gate_wires.O])

        # 3. 通过 `Polynomial` 类生成 A,B,C 多项式
        self.A = Polynomial(A_values, Basis.LAGRANGE)
        self.B = Polynomial(B_values, Basis.LAGRANGE)
        self.C = Polynomial(C_values, Basis.LAGRANGE)

        # 4. 生成 A,B,C 多项式的 KZG 承诺
        a_1 = setup.commit(self.A)
        b_1 = setup.commit(self.B)
        c_1 = setup.commit(self.C)

        # 5. 验证门约束等式是否成立
        # Sanity check that witness fulfils gate constraints
        assert (
            self.A * self.pk.QL
            + self.B * self.pk.QR
            + self.A * self.B * self.pk.QM
            + self.C * self.pk.QO
            + self.PI
            + self.pk.QC
            == Polynomial([Scalar(0)] * group_order, Basis.LAGRANGE)
        )

        return Message1(a_1, b_1, c_1)

Round 2: 生成 Permutation Accumulator 多项式 Z 的 KZG 承诺

参考文章 理解 PLONK(三):置换证明

要给 Z 生成承诺,首先要构造Z,然后可以直接对多项式生成 KZG 承诺。

大体流程:

  1. 初始化Z 的点值数组 Z_values 第一个值为 1
  2. 在 group_order 内,依次用上一次的值乘以当前的累乘因子,获得当前 Z_values 的值
  3. 确保最后一项为 1(具体原理请看上面的文章)
  4. 检查生成值的有效性
  5. 用 Lagrange 形式构造多项式
  6. 生成 KZG 承诺
    def round_2(self) -> Message2:
        group_order = self.group_order
        setup = self.setup

        Z_values = [Scalar(1)]
        roots_of_unity = Scalar.roots_of_unity(group_order)
        for i in range(group_order):
            Z_values.append(
                Z_values[-1]
                * self.rlc(self.A.values[i], roots_of_unity[i])
                * self.rlc(self.B.values[i], 2 * roots_of_unity[i])
                * self.rlc(self.C.values[i], 3 * roots_of_unity[i])
                / self.rlc(self.A.values[i], self.pk.S1.values[i])
                / self.rlc(self.B.values[i], self.pk.S2.values[i])
                / self.rlc(self.C.values[i], self.pk.S3.values[i])
            )
        assert Z_values.pop() == 1

        # Sanity-check that Z was computed correctly
        for i in range(group_order):
            assert (
                self.rlc(self.A.values[i], roots_of_unity[i])
                * self.rlc(self.B.values[i], 2 * roots_of_unity[i])
                * self.rlc(self.C.values[i], 3 * roots_of_unity[i])
            ) * Z_values[i] - (
                self.rlc(self.A.values[i], self.pk.S1.values[i])
                * self.rlc(self.B.values[i], self.pk.S2.values[i])
                * self.rlc(self.C.values[i], self.pk.S3.values[i])
            ) * Z_values[
                (i + 1) % group_order
            ] == 0

        Z = Polynomial(Z_values, Basis.LAGRANGE)
        z_1 = setup.commit(Z)
        print("Permutation accumulator polynomial successfully generated")

        self.Z = Z
        return Message2(z_1)

其中 rlc 的定义:

    def rlc(self, term_1, term_2):
        return term_1 + term_2 * self.beta + self.gamma

Round 3: 生成商多项式的承诺

相关知识可以参考文章 理解 PLONK(四):算术约束与拷贝约束

大体流程:

  1. 构造消失多项式(Vanishing Polynomial): ZH_coeff
  2. 构造电路的门约束多项式: gate_constraints_coeff
  3. 构造 Copy Constraints 的多项式: permutation_grand_product
  4. 构造 Copy Constraints 第一个值为 1 这个约束的多项式: permutation_first_row_coeff
  5. 求出商多项式 quotient polynomial: T_coeff
  6. 计算商多项式的 KZG 承诺
    def round_3(self) -> Message3:
        group_order = self.group_order
        setup = self.setup

        # Compute the quotient polynomial

        alpha = self.alpha

        roots_of_unity = Scalar.roots_of_unity(group_order)

        A_coeff, B_coeff, C_coeff, S1_coeff, S2_coeff, S3_coeff, Z_coeff, QL_coeff, QR_coeff, QM_coeff, QO_coeff, QC_coeff, PI_coeff = (
            x.ifft()
            for x in (
                self.A,
                self.B,
                self.C,
                self.pk.S1,
                self.pk.S2,
                self.pk.S3,
                self.Z,
                self.pk.QL,
                self.pk.QR,
                self.pk.QM,
                self.pk.QO,
                self.pk.QC,
                self.PI,
            )
        )

        L0_coeff = (
            Polynomial([Scalar(1)] + [Scalar(0)] * (group_order - 1), Basis.LAGRANGE)
        ).ifft()

        # x^8 - 1 coeffs are [-1, 0, 0, 0, 0, 0, 0, 0, 1]
        # which needs 9 points(n + 1) to determine the polynomial
        ZH_array = [Scalar(-1)] + [Scalar(0)] * (group_order - 1) + [Scalar(1)]
        ZH_coeff = Polynomial(ZH_array, Basis.MONOMIAL)

        gate_constraints_coeff = (
            A_coeff * QL_coeff
            + B_coeff * QR_coeff
            + A_coeff * B_coeff * QM_coeff
            + C_coeff * QO_coeff
            + PI_coeff
            + QC_coeff
        )

        normal_roots = Polynomial(
            roots_of_unity, Basis.LAGRANGE
        )

        roots_coeff = normal_roots.ifft()
        # z * w
        ZW = self.Z.shift(1)
        ZW_coeff = ZW.ifft()

        for i in range(group_order):
            assert (
                self.rlc(self.A.values[i], roots_of_unity[i])
                * self.rlc(self.B.values[i], 2 * roots_of_unity[i])
                * self.rlc(self.C.values[i], 3 * roots_of_unity[i])
            ) * self.Z.values[i] - (
                self.rlc(self.A.values[i], self.pk.S1.values[i])
                * self.rlc(self.B.values[i], self.pk.S2.values[i])
                * self.rlc(self.C.values[i], self.pk.S3.values[i])
            ) * ZW.values[
                i % group_order
            ] == 0

        permutation_grand_product_coeff = (
            (
                self.rlc(A_coeff, roots_coeff)
                * self.rlc(B_coeff, roots_coeff * Scalar(2))
                * self.rlc(C_coeff, roots_coeff * Scalar(3))
            )
            * Z_coeff
            - (
                self.rlc(A_coeff, S1_coeff)
                * self.rlc(B_coeff, S2_coeff)
                * self.rlc(C_coeff, S3_coeff)
            )
            * ZW_coeff
        )

        permutation_first_row_coeff = (Z_coeff - Scalar(1)) * L0_coeff

        all_constraints = (
            gate_constraints_coeff
            + permutation_grand_product_coeff * alpha
            + permutation_first_row_coeff * alpha**2
        )

        # quotient polynomial
        T_coeff = all_constraints / ZH_coeff

        print("Generated the quotient polynomial")

        W_t = setup.commit(T_coeff)

        self.A_coeff = A_coeff
        self.B_coeff = B_coeff
        self.C_coeff = C_coeff
        self.S1_coeff = S1_coeff
        self.S2_coeff = S2_coeff
        self.S3_coeff = S3_coeff
        self.Z_coeff = Z_coeff
        self.ZW_coeff = ZW_coeff
        self.QL_coeff = QL_coeff
        self.QR_coeff = QR_coeff
        self.QM_coeff = QM_coeff
        self.QO_coeff = QO_coeff
        self.QC_coeff = QC_coeff
        self.PI_coeff = PI_coeff
        self.T_coeff = T_coeff

        return Message3(W_t)

Round 4: 对各个多项式在一个随机的 zeta 点求值

相关知识可以参考文章 理解 PLONK(四):算术约束与拷贝约束

这一步比较简单,对各个多项式在一个随机的 zeta 点求值。这里有一个知识点,就是如何获取 zeta 这个随机值,使用的方法叫做Fiat-Shamir 变换,可以将一个需要 prover 和 verifier 进行交互的证明转化成不需要交互的证明,简单介绍可以参考 这篇文章,代码可以参考 这里

    def round_4(self) -> Message4:
        group_order = self.group_order
        zeta = self.zeta

        a_eval = self.A_coeff.coeff_eval(zeta)
        b_eval = self.B_coeff.coeff_eval(zeta)
        c_eval = self.C_coeff.coeff_eval(zeta)
        s1_eval = self.S1_coeff.coeff_eval(zeta)
        s2_eval = self.S2_coeff.coeff_eval(zeta)
        s3_eval = self.S3_coeff.coeff_eval(zeta)
        root_of_unity = Scalar.root_of_unity(group_order)
        z_eval = self.Z_coeff.coeff_eval(zeta)
        zw_eval = self.Z_coeff.coeff_eval(zeta * root_of_unity)
        ql_eval = self.QL_coeff.coeff_eval(zeta)
        qr_eval = self.QR_coeff.coeff_eval(zeta)
        qm_eval = self.QM_coeff.coeff_eval(zeta)
        qo_eval = self.QO_coeff.coeff_eval(zeta)
        qc_eval = self.QC_coeff.coeff_eval(zeta)
        t_eval = self.T_coeff.coeff_eval(zeta)

        self.a_eval = a_eval
        self.b_eval = b_eval
        self.c_eval = c_eval
        self.ql_eval = ql_eval
        self.qr_eval = qr_eval
        self.qm_eval = qm_eval
        self.qo_eval = qo_eval
        self.qc_eval = qc_eval
        self.s1_eval = s1_eval
        self.s2_eval = s2_eval
        self.s3_eval = s3_eval
        self.z_eval = z_eval
        self.zw_eval = zw_eval
        self.t_eval = t_eval

        return Message4(
            a_eval,
            b_eval,
            c_eval,
            ql_eval,
            qr_eval,
            qm_eval,
            qo_eval,
            qc_eval,
            s1_eval,
            s2_eval,
            s3_eval,
            z_eval,
            zw_eval,
            t_eval
        )

coeff_eval 是一个多项式求值的函数。具体实现可以看这里

Round 5: 对每个多项式生成 KZG 承诺中需要的两个承诺

相关知识可以参考文章 理解 PLONK(四):算术约束与拷贝约束

这一步也比较简单,生成所需的 KZG 承诺,为 verifier 进行 verify 做准备。

    def round_5(self) -> Message5:
        W_a, W_a_quot = self.generate_commitment(self.A_coeff, self.a_eval)
        W_b, W_b_quot = self.generate_commitment(self.B_coeff, self.b_eval)
        W_c, W_c_quot = self.generate_commitment(self.C_coeff, self.c_eval)
        W_ql, W_ql_quot = self.generate_commitment(self.QL_coeff, self.ql_eval)
        W_qr, W_qr_quot = self.generate_commitment(self.QR_coeff, self.qr_eval)
        W_qm, W_qm_quot = self.generate_commitment(self.QM_coeff, self.qm_eval)
        W_qo, W_qo_quot = self.generate_commitment(self.QO_coeff, self.qo_eval)
        W_qc, W_qc_quot = self.generate_commitment(self.QC_coeff, self.qc_eval)
        W_s1, W_s1_quot = self.generate_commitment(self.S1_coeff, self.s1_eval)
        W_s2, W_s2_quot = self.generate_commitment(self.S2_coeff, self.s2_eval)
        W_s3, W_s3_quot = self.generate_commitment(self.S3_coeff, self.s3_eval)
        W_z, W_z_quot = self.generate_commitment(self.Z_coeff, self.z_eval)
        W_zw, W_zw_quot = self.generate_commitment(self.ZW_coeff, self.zw_eval)
        W_t, W_t_quot = self.generate_commitment(self.T_coeff, self.t_eval)

        print("Generated final quotient witness polynomials")
        return Message5(
            W_a, W_a_quot,
            W_b, W_b_quot,
            W_c, W_c_quot,
            W_ql, W_ql_quot,
            W_qr, W_qr_quot,
            W_qm, W_qm_quot,
            W_qo, W_qo_quot,
            W_qc, W_qc_quot,
            W_s1, W_s1_quot,
            W_s2, W_s2_quot,
            W_s3, W_s3_quot,
            W_z, W_z_quot,
            W_zw, W_zw_quot,
            W_t, W_t_quot,
        )

生成承诺的代码:


    def generate_commitment(self, coeff: Polynomial, eval: Scalar):
        setup = self.setup
        zeta = self.zeta
        # Polynomial for (X - zeta)
        ZH_zeta_coeff = Polynomial([-zeta, Scalar(1)], Basis.MONOMIAL)
        quot_coeff = (coeff - eval) / ZH_zeta_coeff
        # witness for polynomial itself
        w = setup.commit(coeff)
        # witness for quotient polynomial
        w_quot = setup.commit(quot_coeff)
        return w, w_quot

Verify

再回顾一下测试中的代码是这样进行验证的:

def verifier_test(setup, proof, group_order):
    print("Beginning verifier test")
    program = Program(["e public", "c <== a * b", "e <== c * d"], group_order)
    public = [60]
    vk = setup.verification_key(program.common_preprocessed_input())
    assert vk.verify_proof(group_order, proof, public)
    print("Verifier test success")

VerificationKey 的代码在这里,最核心的方法是 verify_proof

verifier 主要做两件事情:

  1. 验证 KZG 承诺,保证多项式是和所承诺的一致
  2. 验证最终组合出来的多项式求值的相等性
    def verify_proof(self, group_order: int, pf, public=[]) -> bool:
        # 4. Compute challenges
        beta, gamma, alpha, zeta, v, u = self.compute_challenges(pf)
        proof = pf.flatten()

        # 5. Compute zero polynomial evaluation Z_H(ζ) = ζ^n - 1
        ZH_ev = zeta**group_order - 1

        # 6. Compute Lagrange polynomial evaluation L_0(ζ)
        L0_ev = ZH_ev / (group_order * (zeta - 1))

        # 7. Compute public input polynomial evaluation PI(ζ).
        PI = Polynomial(
            [Scalar(-x) for x in public]
            + [Scalar(0) for _ in range(group_order - len(public))],
            Basis.LAGRANGE,
        )
        PI_ev = PI.barycentric_eval(zeta)

        # verify KZG10 commitment
        self.verify_commitment(proof, proof["W_a"], "W_a_quot", "a_eval", zeta)
        self.verify_commitment(proof, proof["W_b"], "W_b_quot", "b_eval", zeta)
        self.verify_commitment(proof, proof["W_c"], "W_c_quot", "c_eval", zeta)
        self.verify_commitment(proof, proof["W_z"], "W_z_quot", "z_eval", zeta)
        self.verify_commitment(proof, proof["W_zw"], "W_zw_quot", "zw_eval", zeta)
        self.verify_commitment(proof, proof["W_t"], "W_t_quot", "t_eval", zeta)
        self.verify_commitment(proof, self.Ql, "W_ql_quot", "ql_eval", zeta)
        self.verify_commitment(proof, self.Qr, "W_qr_quot", "qr_eval", zeta)
        self.verify_commitment(proof, self.Qm, "W_qm_quot", "qm_eval", zeta)
        self.verify_commitment(proof, self.Qo, "W_qo_quot", "qo_eval", zeta)
        self.verify_commitment(proof, self.Qc, "W_qc_quot", "qc_eval", zeta)
        self.verify_commitment(proof, self.S1, "W_s1_quot", "s1_eval", zeta)
        self.verify_commitment(proof, self.S2, "W_s2_quot", "s2_eval", zeta)
        self.verify_commitment(proof, self.S3, "W_s3_quot", "s3_eval", zeta)

        a_eval = proof["a_eval"]
        b_eval = proof["b_eval"]
        c_eval = proof["c_eval"]
        ql_eval = proof["ql_eval"]
        qr_eval = proof["qr_eval"]
        qm_eval = proof["qm_eval"]
        qo_eval = proof["qo_eval"]
        qc_eval = proof["qc_eval"]
        s1_eval = proof["s1_eval"]
        s2_eval = proof["s2_eval"]
        s3_eval = proof["s3_eval"]
        z_eval = proof["z_eval"]
        zw_eval = proof["zw_eval"]
        t_eval = proof["t_eval"]

        f_eval = (
            (a_eval + beta * zeta + gamma)
            * (b_eval + beta * zeta * 2 + gamma)
            * (c_eval + beta * zeta * 3 + gamma)
        )
        g_eval = (
            (a_eval + beta * s1_eval + gamma)
            * (b_eval + beta * s2_eval + gamma)
            * (c_eval + beta * s3_eval + gamma)
        )

        gate_constraints_eval = (
            ql_eval * a_eval
            + qr_eval * b_eval
            + qm_eval * a_eval * b_eval
            + qo_eval * c_eval
            + qc_eval
            + PI_ev
        )

        permutation_grand_product_eval = z_eval * f_eval - zw_eval * g_eval

        permutation_first_row_eval = L0_ev * (z_eval - 1)

        left = (
            gate_constraints_eval
            + alpha * permutation_grand_product_eval
            +  alpha ** 2 * permutation_first_row_eval
        )

        right = t_eval * ZH_ev

        assert left == right

        print("Done equation check for all constraints")
        return True

    # Compute challenges (should be same as those computed by prover)
    def compute_challenges(
        self, proof
    ) -> tuple[Scalar, Scalar, Scalar, Scalar, Scalar, Scalar]:
        transcript = Transcript(b"plonk")
        beta, gamma = transcript.round_1(proof.msg_1)
        alpha, _fft_cofactor = transcript.round_2(proof.msg_2)
        zeta = transcript.round_3(proof.msg_3)
        v = transcript.round_4(proof.msg_4)
        u = transcript.round_5(proof.msg_5)

        return beta, gamma, alpha, zeta, v, u

    def verify_commitment(self, proof, W, W_quot_key, eval_key, zeta):
        W_quot = proof[W_quot_key]
        eval = proof[eval_key]
        ec_comb = ec_lincomb(
            [
                (W, 1),
                (W_quot, zeta),
                (b.G1, -eval),
            ]
        )

        assert b.pairing(self.X_2, W_quot) == b.pairing(b.G2, ec_comb)
        print(f"Done KZG10 commitment check for {eval_key} polynomial")

最后

以上就是 Plonk 协议的代码讲解,接下来建议读者亲自运行一下这个代码,打印其中一些值看看,这样会对协议的了解更加深刻。